diff --git a/.changeset/chain-interface.md b/.changeset/chain-interface.md new file mode 100644 index 00000000..4f7c7f63 --- /dev/null +++ b/.changeset/chain-interface.md @@ -0,0 +1,87 @@ +--- +"@evolution-sdk/evolution": minor +"@evolution-sdk/devnet": minor +--- + +Introduce composable client API and typed wallet system. + +## New composable client API + +Clients are now built by composing a chain context with provider and wallet constructors via `.with()`. TypeScript infers the accumulated capabilities automatically: + +```ts +import { client, preview, kupmios, seedWallet } from "@evolution-sdk/evolution" + +const myClient = client(preview) + .with(kupmios({ kupoUrl: "...", ogmiosUrl: "..." })) + .with(seedWallet({ mnemonic: "..." })) + +// Promise API +const utxos = await myClient.getUtxos(addr) +const signed = await myClient.signTx(tx) + +// Effect API +myClient.Effect.getUtxos(addr).pipe(Effect.flatMap(...)) + +// Transaction building +myClient.newTx().payToAddress({ address: "addr1...", assets: { lovelace: 5_000_000n } }) +``` + +Per-provider constructors: `blockfrost()`, `kupmios()`, `maestro()`, `koios()` + +Per-wallet constructors: `seedWallet()`, `privateKeyWallet()`, `readOnlyWallet()`, `cip30Wallet()` + +## New typed wallet factories + +```ts +import { makeSigningWalletEffect, makePrivateKeyWalletEffect, makeReadOnlyWalletEffect } from "@evolution-sdk/evolution" + +const wallet = Wallet.makeSigningWalletEffect(chain.id, mnemonic) +const address = yield* wallet.address() +const signed = yield* wallet.signTx(tx) +``` + +## Chain descriptor (breaking change) + +`createClient` now requires an explicit `chain` field instead of the optional `network?: string` field: + +```ts +// Before +createClient({ + network: "preprod", + slotConfig: { zeroTime: 1655769600000n, zeroSlot: 86400n, slotLength: 1000 }, + provider: { ... }, + wallet: { ... } +}) + +// After +import { createClient, preprod } from "@evolution-sdk/evolution" + +createClient({ + chain: preprod, + provider: { ... }, + wallet: { ... } +}) +``` + +Built-in chain constants: `mainnet`, `preprod`, `preview`. Use `defineChain` for custom networks: + +```ts +const devnet = defineChain({ + name: "Devnet", + id: 0, + networkMagic: 42, + slotConfig: { zeroTime: 0n, zeroSlot: 0n, slotLength: 1000 }, + epochLength: 432000, +}) +``` + +## Devnet helpers + +`@evolution-sdk/devnet` adds `getChain(cluster)` and `BOOTSTRAP_CHAIN` for constructing a `Chain` from a running local cluster. + +## Other breaking changes + +- `Koios` class renamed to `KoiosProvider` for consistency with `BlockfrostProvider`, `MaestroProvider`, `KupmiosProvider` +- `networkId` property on client objects replaced with `chain: Chain` +- `createClient` is deprecated in favour of the composable `client()` API diff --git a/README.md b/README.md index f06db84e..096c64d9 100644 --- a/README.md +++ b/README.md @@ -21,25 +21,21 @@ Evolution SDK is a **TypeScript-first** Cardano development framework. Define your data schemas and build transactions with full type safety. You'll get back strongly typed, validated results with comprehensive error handling. ```typescript -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" // Create a client with wallet and provider -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", +const sdkClient = client(preprod) + .with(blockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", + })) + .with(seedWallet({ mnemonic: "your twelve word mnemonic phrase here...", accountIndex: 0 - } -}) + })) // Build a transaction with full type safety -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: "addr_test1qz...", @@ -74,36 +70,32 @@ npm install @evolution-sdk/evolution ## Quick Start ```typescript -import { Core, createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet, Address } from "@evolution-sdk/evolution" // Work with addresses - convert between formats const bech32 = "addr1qx2kd28nq8ac5prwg32hhvudlwggpgfp8utlyqxu6wqgz62f79qsdmm5dsknt9ecr5w468r9ey0fxwkdrwh08ly3tu9sy0f4qd" // Parse Bech32 to address structure -const address = Core.Address.fromBech32(bech32) +const address = Address.fromBech32(bech32) console.log("Network ID:", address.networkId) console.log("Payment credential:", address.paymentCredential) // Convert to different formats -const hex = Core.Address.toHex(address) -const bytes = Core.Address.toBytes(address) +const hex = Address.toHex(address) +const bytes = Address.toBytes(address) // Build and submit transactions -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", +const sdkClient = client(preprod) + .with(blockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", + })) + .with(seedWallet({ mnemonic: "your mnemonic here...", accountIndex: 0 - } -}) + })) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: bech32, diff --git a/docs/app/(home)/page.tsx b/docs/app/(home)/page.tsx index 40cc562e..b778e666 100644 --- a/docs/app/(home)/page.tsx +++ b/docs/app/(home)/page.tsx @@ -13,17 +13,15 @@ const packageManagers = [ { id: "bun", name: "bun", icon: SiBun, color: "#FBF0DF", command: "bun add @evolution-sdk/evolution" } ] -const quickStartCode = `import { Address, Assets, createClient } from "@evolution-sdk/evolution" +const quickStartCode = `import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" // 1. Create a client -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "...", projectId: "..." }, - wallet: { type: "seed", mnemonic: "your 24 words here" } -}) +const sdkClient = client(preprod) + .with(blockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: "..." })) + .with(seedWallet({ mnemonic: "your 24 words here" })) // 2. Build a transaction -const tx = await client.newTx() +const tx = await sdkClient.newTx() .payToAddress({ address: Address.fromBech32("addr_test1..."), assets: Assets.fromLovelace(5_000_000n) diff --git a/docs/content/docs/addresses/construction.mdx b/docs/content/docs/addresses/construction.mdx index 3aaa713d..6f2be570 100644 --- a/docs/content/docs/addresses/construction.mdx +++ b/docs/content/docs/addresses/construction.mdx @@ -51,14 +51,14 @@ const bech32 = Address.toBech32(address) ## Getting Your Wallet Address ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const myAddress = await client.address() +const myAddress = await sdkClient.getAddress() ``` ## Next Steps diff --git a/docs/content/docs/advanced/architecture.mdx b/docs/content/docs/advanced/architecture.mdx index 41d37dff..7be46705 100644 --- a/docs/content/docs/advanced/architecture.mdx +++ b/docs/content/docs/advanced/architecture.mdx @@ -32,7 +32,7 @@ Selection → Collateral → Change Creation → Fee Calculation → Balance → ```typescript // These don't execute immediately — they record operations -const builder = client.newTx() +const builder = sdkClient.newTx() .payToAddress({ ... }) // Records a "pay" program .collectFrom({ ... }) // Records a "collect" program .mintAssets({ ... }) // Records a "mint" program diff --git a/docs/content/docs/advanced/custom-providers.mdx b/docs/content/docs/advanced/custom-providers.mdx index 51028fcb..295c5afb 100644 --- a/docs/content/docs/advanced/custom-providers.mdx +++ b/docs/content/docs/advanced/custom-providers.mdx @@ -38,27 +38,21 @@ interface ProviderEffect { ## Using a Provider ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, kupmios } from "@evolution-sdk/evolution" // Blockfrost -const blockfrostClient = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const blockfrostClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - } -}) +})) // Kupo/Ogmios -const kupmiosClient = createClient({ - network: "preprod", - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", +const kupmiosClient = client(preprod) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - } -}) +})) ``` ## Next Steps diff --git a/docs/content/docs/advanced/error-handling.mdx b/docs/content/docs/advanced/error-handling.mdx index ca063a68..e8c6a7f2 100644 --- a/docs/content/docs/advanced/error-handling.mdx +++ b/docs/content/docs/advanced/error-handling.mdx @@ -20,16 +20,18 @@ Evolution SDK is built on Effect, providing structured error handling with typed The standard `.build()`, `.sign()`, `.submit()` methods return Promises. Errors throw as exceptions: ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) try { - const tx = await client + const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -50,16 +52,18 @@ Use `.buildEffect()` for composable error handling with Effect: ```typescript twoslash import { Effect } from "effect" -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) // Build returns an Effect — errors are values, not exceptions -const program = client +const program = sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -76,15 +80,17 @@ const program = client Use `.buildEither()` for explicit success/failure without exceptions: ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const result = await client +const result = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), diff --git a/docs/content/docs/advanced/performance.mdx b/docs/content/docs/advanced/performance.mdx index 911ac912..6cebde55 100644 --- a/docs/content/docs/advanced/performance.mdx +++ b/docs/content/docs/advanced/performance.mdx @@ -18,15 +18,17 @@ Evolution SDK provides several configuration options to optimize transaction bui The default algorithm is `"largest-first"`. You can also provide a custom coin selection function: ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae"), @@ -40,15 +42,17 @@ const tx = await client The `unfrack` option optimizes your wallet's UTxO structure during transaction building: ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), diff --git a/docs/content/docs/advanced/typescript.mdx b/docs/content/docs/advanced/typescript.mdx index c77dee38..2364647f 100644 --- a/docs/content/docs/advanced/typescript.mdx +++ b/docs/content/docs/advanced/typescript.mdx @@ -12,7 +12,7 @@ Evolution SDK leverages TypeScript's type system for safety and developer experi TSchema definitions infer their TypeScript types automatically: ```typescript twoslash -import { TSchema } from "@evolution-sdk/evolution" +import { TSchema, blockfrost, seedWallet, client } from "@evolution-sdk/evolution" const MySchema = TSchema.Struct({ amount: TSchema.Integer, @@ -30,7 +30,7 @@ type MyType = typeof MySchema.Type Core types like `Address`, `TransactionHash`, and `PolicyId` are branded — they're structurally identical to their base types but won't accidentally mix: ```typescript twoslash -import { Address } from "@evolution-sdk/evolution" +import { Address, blockfrost, seedWallet, client } from "@evolution-sdk/evolution" // Address.Address is a branded type const addr = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63") @@ -42,20 +42,22 @@ const addr = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5 The client type narrows based on configuration: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" // Provider-only client (no wallet) — can query but not sign -const queryClient = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" } -}) +const queryClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) // Signing client (wallet + provider) — full capabilities -const signingClient = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}) +const signingClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})) ``` ## Effect Integration @@ -66,10 +68,10 @@ The SDK provides both Promise and Effect APIs: import { Effect } from "effect" // Promise API — standard async/await -// const tx = await client.newTx().payToAddress({...}).build() +// const tx = await sdkClient.newTx().payToAddress({...}).build() // Effect API — composable error handling -// const program = client.newTx().payToAddress({...}).buildEffect() +// const program = sdkClient.newTx().payToAddress({...}).buildEffect() // Effect.runPromise(program) ``` @@ -78,7 +80,7 @@ import { Effect } from "effect" The SDK uses namespace exports for tree-shaking optimization: ```typescript twoslash -import { Address, Assets, Data, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Data, client, blockfrost, seedWallet } from "@evolution-sdk/evolution" // Each namespace provides related functions // Address.fromBech32(), Address.toHex(), Address.toBytes() diff --git a/docs/content/docs/architecture/deferred-execution.mdx b/docs/content/docs/architecture/deferred-execution.mdx index d950085e..2769e6fc 100644 --- a/docs/content/docs/architecture/deferred-execution.mdx +++ b/docs/content/docs/architecture/deferred-execution.mdx @@ -66,8 +66,6 @@ graph TB **[3] Second build() Call**: Creates new fresh `Ref`, re-executes all programs, returns independent transaction. No state shared with first execution. -**[3] Second build() Call**: Creates new fresh `Ref`, re-executes all programs, returns independent transaction. No state shared with first execution. - ## Execution Sequence Programs execute sequentially in append order. Each program can observe effects of previous programs within the same build cycle: @@ -102,8 +100,6 @@ sequenceDiagram **[3] Transaction Assembly**: Final state is read, transaction body constructed, witnesses prepared. -**[3] Transaction Assembly**: Final state is read, transaction body constructed, witnesses prepared. - ## Integration Points Deferred execution integrates with other architectural layers: diff --git a/docs/content/docs/architecture/index.mdx b/docs/content/docs/architecture/index.mdx index af8ab372..623952ef 100644 --- a/docs/content/docs/architecture/index.mdx +++ b/docs/content/docs/architecture/index.mdx @@ -106,30 +106,30 @@ The architecture maintains strict boundaries between layers. The client never di MinimalClient + [*] --> BaseClient: client(chain) - MinimalClient --> ProviderOnlyClient: attachProvider() - MinimalClient --> WalletClient: attachWallet() - MinimalClient --> FullClient: attach(provider, wallet) + BaseClient --> ProviderClient: .with(provider) + BaseClient --> WalletClient: .with(wallet) + BaseClient --> FullClient: .with(provider).with(wallet) - ProviderOnlyClient --> FullClient: attachWallet() - WalletClient --> FullClient: attachProvider() + ProviderClient --> FullClient: .with(wallet) + WalletClient --> FullClient: .with(provider) FullClient --> [*] - MinimalClient: [1] MinimalClient - ProviderOnlyClient: [2] ProviderOnlyClient - WalletClient: [3] WalletClient - FullClient: [4] FullClient + BaseClient: [1] Base Client + ProviderClient: [2] Provider Client + WalletClient: [3] Wallet Client + FullClient: [4] Full Client `} /> -**[1] MinimalClient**: Network context only, no blockchain access or signing capability +**[1] Base Client**: Chain context only. No blockchain access or signing capability. Created by `client(chain)`. -**[2] ProviderOnlyClient**: Can query blockchain and submit transactions, cannot sign +**[2] Provider Client**: Can query blockchain and submit transactions, cannot sign. Created by `.with(provider)`. -**[3] WalletClient**: Can sign transactions and messages, must attach provider to build transactions +**[3] Wallet Client**: Can sign transactions and messages, must attach provider to build transactions. Created by `.with(wallet)`. -**[4] FullClient**: Either ReadOnlyClient (query + unsigned transactions) or SigningClient (query + sign + submit) depending on wallet type +**[4] Full Client**: Either read-only (query + unsigned transactions) or signing (query + sign + submit) depending on wallet type. Created by chaining `.with(provider)` and `.with(wallet)`. ## When This Matters diff --git a/docs/content/docs/architecture/provider-layer.mdx b/docs/content/docs/architecture/provider-layer.mdx index d124d14b..2fd93a0a 100644 --- a/docs/content/docs/architecture/provider-layer.mdx +++ b/docs/content/docs/architecture/provider-layer.mdx @@ -21,8 +21,6 @@ The architecture establishes a thin interface capturing what transaction buildin The abstraction remains thin by design—it does not attempt to expose every provider's unique features. Focused scope keeps the interface stable and implementations maintainable. -The abstraction remains thin by design—it does not attempt to expose every provider's unique features. Focused scope keeps the interface stable and implementations maintainable. - ## Provider Interface Contract All providers implement the same core operations required for transaction building: @@ -69,8 +67,6 @@ graph TD **[2] Provider Implementations**: Each translates interface calls to their native API. Application code depends only on interface, never on specific implementation. -**[2] Provider Implementations**: Each translates interface calls to their native API. Application code depends only on interface, never on specific implementation. - ## Type-Driven Configuration Provider configuration uses discriminated unions to enforce correctness at compile time: @@ -83,10 +79,10 @@ graph LR Config --> Type - Type -->|blockfrost| BF[url + projectId] + Type -->|blockfrost| BF[baseUrl + projectId] Type -->|kupmios| KP[kupoUrl + ogmiosUrl] Type -->|maestro| MA[network + apiKey] - Type -->|koios| KO[url + token?] + Type -->|koios| KO[baseUrl + token?] BF --> Factory["[2] Provider Factory"] KP --> Factory @@ -107,9 +103,7 @@ graph LR **[3] Provider Instance**: Implements unified interface. Type system guarantees all required configuration is present. -TypeScript enforces: `type: "blockfrost"` requires `url` and `projectId`. `type: "kupmios"` requires `kupoUrl` and `ogmiosUrl`. Mismatches are compilation errors, not runtime failures. - -TypeScript enforces: `type: "blockfrost"` requires `url` and `projectId`. `type: "kupmios"` requires `kupoUrl` and `ogmiosUrl`. Mismatches are compilation errors, not runtime failures. +TypeScript enforces: `type: "blockfrost"` requires `baseUrl` and `projectId`. `type: "kupmios"` requires `kupoUrl` and `ogmiosUrl`. Mismatches are compilation errors, not runtime failures. ## Integration Points @@ -120,7 +114,7 @@ The provider layer integrates with other architectural components: - Available UTxOs at wallet addresses (coin selection) - Script evaluation (calculating execution units for Plutus scripts) -**Client Configuration**: Provider is attached via `createClient()` or `attachProvider()`. The client stores the provider reference and passes it to transaction builders. +**Client Configuration**: Provider is attached via `.with(provider)` on the composable client. The client stores the provider reference and passes it to transaction builders. **Error Handling**: Provider methods return `Effect`. All provider-specific errors (HTTP failures, WebSocket disconnections, rate limits, authentication) normalize to `ProviderError` with consistent structure. Application code handles one error type, not provider-specific exceptions. diff --git a/docs/content/docs/architecture/wallet-layer.mdx b/docs/content/docs/architecture/wallet-layer.mdx index 52420065..c0fc15d6 100644 --- a/docs/content/docs/architecture/wallet-layer.mdx +++ b/docs/content/docs/architecture/wallet-layer.mdx @@ -21,8 +21,6 @@ The architecture encodes capability in types. A `ReadOnlyWallet` produces a `Rea This separation provides security by design: applications needing only monitoring cannot accidentally expose signing capability. The type system makes "read-only" genuinely read-only at the language level, not through defensive programming. -This separation provides security by design: applications needing only monitoring cannot accidentally expose signing capability. The type system makes "read-only" genuinely read-only at the language level, not through defensive programming. - ## Wallet Capability Hierarchy Wallets separate into two capability levels, with signing wallets further divided by key management approach: @@ -54,7 +52,7 @@ graph TD `} /> **[1] Wallet Base**: Common operations all wallets support: -- `address()` - Get wallet address +- `getAddress()` - Get wallet address - `getUtxos()` - Query UTxOs at wallet address - `getBalance()` - Query total balance @@ -69,8 +67,6 @@ Three signing implementations: - **PrivateKeyWallet**: Extended private key (xprv) - **ApiWallet**: CIP-30 browser wallet API (Nami, Eternl, Flint, hardware wallets) -- **ApiWallet**: CIP-30 browser wallet API (Nami, Eternl, Flint, hardware wallets) - ## Client Type Determination Wallet capability determines client type through conditional types. The compiler selects client type based on wallet type: @@ -110,13 +106,11 @@ graph LR **[3] SigningClient / ApiWalletClient**: Created from `SigningWallet` or `ApiWallet`. Transaction builder's `build()` returns `SignBuilder` which has `sign()` method. Type system guarantees signing capability exists. -**[3] SigningClient / ApiWalletClient**: Created from `SigningWallet` or `ApiWallet`. Transaction builder's `build()` returns `SignBuilder` which has `sign()` method. Type system guarantees signing capability exists. - ## Integration Points The wallet layer integrates with other architectural components: -**Client Factory**: `createClient(config)` uses conditional types to return appropriate client type based on wallet. Type narrowing happens at construction—no runtime type guards needed in application code. +**Client Factory**: The composable `.with(wallet)` chain uses conditional types to return the appropriate client type based on wallet. Type narrowing happens at construction—no runtime type guards needed in application code. **Transaction Builder**: Builder type (`ReadOnlyTransactionBuilder` vs `SigningTransactionBuilder`) determined by wallet capability. Read-only builders cannot produce `SignBuilder`, only `TransactionResultBase`. diff --git a/docs/content/docs/assets/metadata.mdx b/docs/content/docs/assets/metadata.mdx index 1ec3de15..a734b3c1 100644 --- a/docs/content/docs/assets/metadata.mdx +++ b/docs/content/docs/assets/metadata.mdx @@ -18,17 +18,19 @@ Transaction metadata lets you attach arbitrary data to transactions without affe ## Attach a Message (CIP-20) ```typescript twoslash -import { Address, Assets, TransactionMetadatum, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, TransactionMetadatum, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const messageMetadata: TransactionMetadatum.TransactionMetadatum -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -49,18 +51,20 @@ await signed.submit() Chain multiple `attachMetadata` calls for different labels: ```typescript twoslash -import { Address, Assets, TransactionMetadatum, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, TransactionMetadatum, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const messageMetadata: TransactionMetadatum.TransactionMetadatum declare const nftMetadata: TransactionMetadatum.TransactionMetadatum -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), diff --git a/docs/content/docs/clients/architecture.mdx b/docs/content/docs/clients/architecture.mdx index f4bae19e..c04b6731 100644 --- a/docs/content/docs/clients/architecture.mdx +++ b/docs/content/docs/clients/architecture.mdx @@ -32,23 +32,17 @@ interface ReadOnlyWalletConfig { ### Backend Transaction Building ```typescript twoslash -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; - -// Backend: Create provider client, then attach read-only wallet -const providerClient = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; + +// Backend: Create provider sdkClient, then attach read-only wallet +const providerClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Attach user's address as read-only wallet (expects bech32 string) -const backendClient = providerClient.attachWallet({ - type: "read-only", - address: "addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c" -}); +const backendClient = providerClient.with(readOnlyWallet("addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c")); // Build unsigned transaction const builder = backendClient.newTx(); @@ -87,20 +81,18 @@ Frontend applications connect to user wallets through CIP-30 but never have prov ### Implementation ```typescript -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; // 1. Connect wallet declare const cardano: any; const walletApi = await cardano.eternl.enable(); // 2. Create signing-only client -const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } -}); +const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); // 3. Get user address for backend -const address = await client.address(); +const address = await sdkClient.getAddress(); // 4. Send address to backend, receive unsigned tx CBOR const unsignedTxCbor = await fetch("/api/build-tx", { @@ -109,10 +101,10 @@ const unsignedTxCbor = await fetch("/api/build-tx", { }).then(r => r.json()).then(data => data.txCbor); // 5. Sign with user wallet -const witnessSet = await client.signTx(unsignedTxCbor); +const witnessSet = await sdkClient.signTx(unsignedTxCbor); -// 6. Submit -const txHash = await client.submitTx(unsignedTxCbor); +// 6. Submit via wallet +const txHash = await sdkClient.walletSubmitTx(unsignedTxCbor); console.log("Transaction submitted:", txHash); ``` @@ -138,26 +130,20 @@ Backend services use read-only wallets configured with user addresses to build u ### Implementation ```typescript -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; // Backend endpoint export async function buildTransaction(userAddress: string) { // Create read-only client with user's address - const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", + const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - }, - wallet: { - type: "read-only", - address: userAddress - } - }); +})) + .with(readOnlyWallet(userAddress)); // Build unsigned transaction - const builder = client.newTx(); + const builder = sdkClient.newTx(); builder.payToAddress({ address: Address.fromBech32("addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"), assets: Assets.fromLovelace(5_000_000n) @@ -193,7 +179,7 @@ export type BuildPaymentResponse = { txCbor: string }; // @filename: frontend.ts // ===== Frontend (Browser) ===== -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; import type { BuildPaymentResponse } from "./shared"; declare const cardano: any; @@ -201,13 +187,11 @@ declare const cardano: any; async function sendPayment(recipientAddress: string, lovelace: bigint) { // 1. Connect user wallet const walletApi = await cardano.eternl.enable(); - const walletClient = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } - }); + const walletClient = client(mainnet) + .with(cip30Wallet(walletApi)); // 2. Get user address (returns Core Address, convert to bech32 for backend) - const userAddress = Address.toBech32(await walletClient.address()); + const userAddress = Address.toBech32(await walletClient.getAddress()); // 3. Request backend to build transaction const response = await fetch("/api/build-payment", { @@ -224,15 +208,15 @@ async function sendPayment(recipientAddress: string, lovelace: bigint) { // 4. Sign with user wallet (prompts user approval) const witnessSet = await walletClient.signTx(txCbor); - // 5. Submit signed transaction - const txHash = await walletClient.submitTx(txCbor); + // 5. Submit via wallet + const txHash = await walletClient.walletSubmitTx(txCbor); return txHash; } // @filename: backend.ts // ===== Backend (Server) ===== -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; export async function buildPayment( userAddressBech32: string, @@ -243,23 +227,17 @@ export async function buildPayment( const recipientAddress = Address.fromBech32(recipientAddressBech32); // Create provider client first, then attach read-only wallet - const providerClient = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", + const providerClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } - }); +})); // Attach user's address as read-only wallet - const client = providerClient.attachWallet({ - type: "read-only", - address: userAddressBech32 - }); + const userClient = providerClient.with(readOnlyWallet(userAddressBech32)); // Build unsigned transaction - const builder = client.newTx(); + const builder = userClient.newTx(); builder.payToAddress({ address: recipientAddress, assets: Assets.fromLovelace(lovelace) @@ -276,8 +254,6 @@ export async function buildPayment( ### Why This Works -### Why This Works - User keys never leave their device. Provider API keys never exposed to frontend. Backend cannot sign transactions (compromised server results in no fund loss). Frontend cannot build transactions alone (no provider access). Both components required for complete transactions. Clean separation of concerns provides scalable architecture. --- @@ -286,8 +262,8 @@ User keys never leave their device. Provider API keys never exposed to frontend. | Method | Seed | Private Key | API Wallet | Read-Only | |--------|------|-------------|------------|-----------| -| `address()` | Yes | Yes | Yes | Yes | -| `rewardAddress()` | Yes | Yes | Yes | Yes | +| `getAddress()` | Yes | Yes | Yes | Yes | +| `getRewardAddress()` | Yes | Yes | Yes | Yes | | `newTx()` | Yes | Yes | No | Yes | | `sign()` | Yes | Yes | Yes | No | | `signTx()` | Yes | Yes | Yes | No | @@ -301,56 +277,39 @@ User keys never leave their device. Provider API keys never exposed to frontend. ```typescript // WRONG - Frontend has no provider -const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } - // No provider! -}); +const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); -const builder = client.newTx(); // Error: Cannot build without provider +const builder = sdkClient.newTx(); // Error: Cannot build without provider ``` **Correct - Frontend signs only:** ```typescript // CORRECT - Frontend signs, backend builds -const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } -}); +const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); // Get tx from backend, then sign -const witnessSet = await client.signTx(txCborFromBackend); +const witnessSet = await sdkClient.signTx(txCborFromBackend); ``` **Error: Backend trying to sign:** ```typescript // WRONG - Backend has no private keys -const client = createClient({ - network: "mainnet", - provider: { /* ... */ }, - wallet: { - type: "read-only", - address: userAddress - } -}); +const sdkClient = client(mainnet) + .with(readOnlyWallet(userAddress)); -await client.sign(); // Error: Cannot sign with read-only wallet +await sdkClient.sign(); // Error: Cannot sign with read-only wallet ``` **Correct - Backend builds only:** ```typescript // CORRECT - Backend builds, frontend signs -const client = createClient({ - network: "mainnet", - provider: { /* ... */ }, - wallet: { - type: "read-only", - address: userAddress - } -}); +const sdkClient = client(mainnet) + .with(readOnlyWallet(userAddress)); const unsignedTx = await builder.build(); // Returns unsigned CBOR ``` diff --git a/docs/content/docs/clients/architecture/frontend-backend.mdx b/docs/content/docs/clients/architecture/frontend-backend.mdx index 168bd616..fdc5b93d 100644 --- a/docs/content/docs/clients/architecture/frontend-backend.mdx +++ b/docs/content/docs/clients/architecture/frontend-backend.mdx @@ -10,8 +10,8 @@ Complete architecture patterns showing proper separation between frontend signin ## Overview Modern web applications require separation of concerns: -- **Frontend**: User wallets for signing (API wallet client) -- **Backend**: Transaction building with provider access (Read-only client) +- **Frontend**: User wallets for signing (API wallet sdkClient) +- **Backend**: Transaction building with provider access (Read-only sdkClient) - **Security**: Keys on user device, provider keys on server This pattern uses two different **client types**: @@ -32,7 +32,7 @@ Frontend applications use API wallet clients (CIP-30) for signing only. They hav **Cannot Do**: Build transactions, query blockchain, fee calculation ```typescript twoslash -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; declare const cardano: any; @@ -40,13 +40,11 @@ declare const cardano: any; const walletApi = await cardano.eternl.enable(); // 2. Create API wallet client (no provider) -const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } -}); +const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); // 3. Get user address to send to backend -const address = await client.address(); +const address = await sdkClient.getAddress(); // 4. Receive unsigned transaction from backend const unsignedTxCbor = await fetch("/api/build-tx", { @@ -56,10 +54,10 @@ const unsignedTxCbor = await fetch("/api/build-tx", { }).then(r => r.json()).then(data => data.txCbor); // 5. Sign with user wallet (prompts approval) -const witnessSet = await client.signTx(unsignedTxCbor); +const witnessSet = await sdkClient.signTx(unsignedTxCbor); -// 6. Submit signed transaction -const txHash = await client.submitTx(unsignedTxCbor); + // 6. Submit via wallet + const txHash = await sdkClient.walletSubmitTx(unsignedTxCbor); console.log("Transaction submitted:", txHash); ``` @@ -81,25 +79,19 @@ Backend services use read-only clients configured with user addresses to build u **Cannot Do**: Sign transactions, access private keys ```typescript twoslash -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; export async function buildTransaction(userAddressBech32: string) { // Create read-only client with user's address (bech32 string) - const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", + const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - }, - wallet: { - type: "read-only", - address: userAddressBech32 - } - }); +})) + .with(readOnlyWallet(userAddressBech32)); // Build unsigned transaction - const builder = client.newTx(); + const builder = sdkClient.newTx(); builder.payToAddress({ address: Address.fromBech32("addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"), assets: Assets.fromLovelace(5000000n) @@ -142,7 +134,7 @@ export type BuildPaymentResponse = { // @filename: frontend.ts // ===== Frontend (Browser) ===== -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; import type { BuildPaymentRequest, BuildPaymentResponse } from "./shared"; declare const cardano: any; @@ -152,13 +144,11 @@ async function sendPayment(recipientAddress: string, lovelace: bigint) { const walletApi = await cardano.eternl.enable(); // 2. Create API wallet client (signing only) - const walletClient = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } - }); + const walletClient = client(mainnet) + .with(cip30Wallet(walletApi)); // 3. Get user address (returns Core Address, convert to bech32 for backend) - const userAddress = Address.toBech32(await walletClient.address()); + const userAddress = Address.toBech32(await walletClient.getAddress()); // 4. Request backend to build transaction const requestBody: BuildPaymentRequest = { @@ -178,15 +168,15 @@ async function sendPayment(recipientAddress: string, lovelace: bigint) { // 5. Sign with user wallet (prompts user approval) const witnessSet = await walletClient.signTx(txCbor); - // 6. Submit signed transaction - const txHash = await walletClient.submitTx(txCbor); + // 6. Submit via wallet + const txHash = await walletClient.walletSubmitTx(txCbor); return txHash; } // @filename: backend.ts // ===== Backend (Server) ===== -import { Address, Assets, Transaction, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Transaction, client, mainnet, blockfrost, cip30Wallet, readOnlyWallet } from "@evolution-sdk/evolution"; import type { BuildPaymentResponse } from "./shared"; export async function buildPayment( @@ -198,21 +188,15 @@ export async function buildPayment( const recipientAddress = Address.fromBech32(recipientAddressBech32); // Create read-only client with user's address (bech32 string) - const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", + const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - }, - wallet: { - type: "read-only", - address: userAddressBech32 - } - }); +})) + .with(readOnlyWallet(userAddressBech32)); // Build unsigned transaction - const builder = client.newTx(); + const builder = sdkClient.newTx(); builder.payToAddress({ address: recipientAddress, assets: Assets.fromLovelace(lovelace) @@ -273,13 +257,10 @@ This architecture provides complete security through proper separation: ```typescript // WRONG - API wallet client has no provider -const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } - // No provider configured! -}); +const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); -const builder = client.newTx(); // Error: Cannot build without provider +const builder = sdkClient.newTx(); // Error: Cannot build without provider ``` **Fix**: Get unsigned transaction from backend instead. @@ -288,16 +269,14 @@ const builder = client.newTx(); // Error: Cannot build without provider ```typescript // CORRECT - API wallet client signs only -const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } -}); +const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); // Get transaction from backend const { txCbor } = await fetch("/api/build-tx").then(r => r.json()); // Sign with user approval -const witnessSet = await client.signTx(txCbor); +const witnessSet = await sdkClient.signTx(txCbor); ``` --- @@ -306,16 +285,10 @@ const witnessSet = await client.signTx(txCbor); ```typescript // WRONG - Read-only client has no private keys -const client = createClient({ - network: "mainnet", - provider: { /* ... */ }, - wallet: { - type: "read-only", - address: userAddress - } -}); - -await client.sign(data); // Error: Cannot sign with read-only wallet +const sdkClient = client(mainnet) + .with(readOnlyWallet(userAddress)); + +await sdkClient.sign(data); // Error: Cannot sign with read-only wallet ``` **Fix**: Return unsigned transaction to frontend for signing. @@ -324,17 +297,11 @@ await client.sign(data); // Error: Cannot sign with read-only wallet ```typescript // CORRECT - Read-only client builds only -const client = createClient({ - network: "mainnet", - provider: { /* ... */ }, - wallet: { - type: "read-only", - address: userAddress - } -}); +const sdkClient = client(mainnet) + .with(readOnlyWallet(userAddress)); // Build unsigned transaction -const builder = client.newTx(); +const builder = sdkClient.newTx(); // ... configure transaction ... const result = await builder.build(); const unsignedTx = await result.toTransaction(); @@ -351,8 +318,8 @@ Understanding what each client type can do: | Method | Full Client | API Wallet Client | Read-Only Client | Provider-Only Client | |--------|-------------|-------------------|------------------|---------------------| -| `address()` | ✅ | ✅ | ✅ | ❌ | -| `rewardAddress()` | ✅ | ✅ | ✅ | ❌ | +| `getAddress()` | ✅ | ✅ | ✅ | ❌ | +| `getRewardAddress()` | ✅ | ✅ | ✅ | ❌ | | `newTx()` | ✅ | ❌ | ✅ | ✅ | | `sign()` | ✅ | ✅ | ❌ | ❌ | | `signTx()` | ✅ | ✅ | ❌ | ❌ | diff --git a/docs/content/docs/clients/client-basics.mdx b/docs/content/docs/clients/client-basics.mdx index 3c7efbd5..673b3570 100644 --- a/docs/content/docs/clients/client-basics.mdx +++ b/docs/content/docs/clients/client-basics.mdx @@ -7,48 +7,78 @@ description: "Learn the basics of creating and using an Evolution SDK client" The Evolution SDK client is your primary interface to the Cardano blockchain. It combines wallet management, provider communication, and transaction building into a single, cohesive API. Once configured, the client handles UTxO selection, fee calculation, and signing—letting you focus on your application logic. -Think of the client as your persistent connection: configure it once with your network, provider, and wallet, then use it throughout your application to build and submit transactions. All operations maintain type safety and composability through Effect-TS. +Think of the client as your persistent connection: configure it once with your chain, provider, and wallet, then use it throughout your application to build and submit transactions. All operations maintain type safety and composability through Effect-TS. ## Creating a Client -Configure your client with three essential pieces: the network (mainnet/preprod/preview), your blockchain provider, and the wallet for signing: +Configure your client with three essential pieces: the **chain** (which Cardano network to target), your blockchain provider, and the wallet for signing: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +})) + .with(seedWallet({ +mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", accountIndex: 0 - } +})); +``` + +## Chain Configuration + +The `chain` field is a rich descriptor, not just a string identifier. It carries network magic, slot timing parameters, and optional block explorer URLs — everything the SDK needs to interact with a specific Cardano network. + +Evolution SDK exports three built-in chain constants: + +| Constant | Network | Network Magic | +|----------|---------|---------------| +| `mainnet` | Production | `764824073` | +| `preprod` | Pre-production testnet | `1` | +| `preview` | Preview testnet | `2` | + +```ts twoslash +import { mainnet, preprod, preview, blockfrost, seedWallet, client } from "@evolution-sdk/evolution"; +``` + +For custom networks — local devnets, private chains, or future Cardano forks — use `defineChain`: + +```ts twoslash +import { defineChain, blockfrost, seedWallet, client } from "@evolution-sdk/evolution"; + +const devnet = defineChain({ + name: "Devnet", + id: 0, + networkMagic: 42, + slotConfig: { zeroTime: 0n, zeroSlot: 0n, slotLength: 1000 }, + epochLength: 432000, }); ``` +The `slotConfig` is embedded in the chain constant and consumed directly by the transaction builder for slot-to-POSIX time conversions. There is no separate `slotConfig` parameter on `client`. + ## The Transaction Workflow Evolution SDK follows a three-stage pattern: build, sign, submit. Each stage returns a new builder with stage-specific methods, preventing invalid operations (like submitting before signing). ### Stage 1: Building -Start with `client.newTx()` and chain operations to specify outputs, metadata, or validity ranges: +Start with `sdkClient.newTx()` and chain operations to specify outputs, metadata, or validity ranges: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}); +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})); -const builder = client.newTx(); +const builder = sdkClient.newTx(); builder.payToAddress({ address: Address.fromBech32("addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y"), @@ -63,15 +93,17 @@ const signBuilder = await builder.build(); Call `.sign()` on the built transaction to create signatures with your wallet: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}); +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})); -const builder = client.newTx(); +const builder = sdkClient.newTx(); builder.payToAddress({ address: Address.fromBech32("addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y"), assets: Assets.fromLovelace(2000000n) }); const signBuilder = await builder.build(); @@ -83,15 +115,17 @@ const submitBuilder = await signBuilder.sign(); Finally, `.submit()` broadcasts the signed transaction to the blockchain and returns the transaction hash: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}); +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})); -const builder = client.newTx(); +const builder = sdkClient.newTx(); builder.payToAddress({ address: Address.fromBech32("addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y"), assets: Assets.fromLovelace(2000000n) }); const signBuilder = await builder.build(); const submitBuilder = await signBuilder.sign(); @@ -105,24 +139,20 @@ console.log("Transaction submitted:", txId); Here's the complete workflow in a single example—from client creation through transaction submission: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 - } -}); +})); // Build transaction -const builder = client.newTx(); +const builder = sdkClient.newTx(); builder.payToAddress({ address: Address.fromBech32("addr_test1qzx9hu8j4ah3auytk0mwcupd69hpc52t0cw39a65ndrah86djs784u92a3m5w475w3w35tyd6v3qumkze80j8a6h5tuqq5xe8y"), assets: Assets.fromLovelace(2000000n) diff --git a/docs/content/docs/clients/index.mdx b/docs/content/docs/clients/index.mdx index e298ab0a..38c7d69c 100644 --- a/docs/content/docs/clients/index.mdx +++ b/docs/content/docs/clients/index.mdx @@ -27,14 +27,13 @@ Different combinations create different client types with distinct capabilities. ## Configuration Pattern -All clients use `createClient` with different configurations: +All clients use `client` with different configurations: ```typescript -createClient({ - network: "mainnet" | "preprod" | "preview", - provider?: ProviderConfig, // Optional - wallet?: WalletConfig // Optional -}) +import { mainnet, preprod, preview, defineChain, client } from "@evolution-sdk/evolution"; + +// Pass a chain — mainnet, preprod, or preview +const sdkClient = client(preprod) ``` **Rules**: diff --git a/docs/content/docs/clients/providers.mdx b/docs/content/docs/clients/providers.mdx index 0789a452..d96113c7 100644 --- a/docs/content/docs/clients/providers.mdx +++ b/docs/content/docs/clients/providers.mdx @@ -14,21 +14,17 @@ Choose Blockfrost for quick setup with hosted infrastructure, or Kupmios for sel Blockfrost offers hosted infrastructure with a free tier, perfect for development and small-scale applications. Setup takes minutes—just get an API key and connect. ```ts twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, preprod, blockfrost, kupmios, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 - } -}); +})); ``` **Setup:** @@ -41,21 +37,17 @@ const client = createClient({ Run your own Cardano node infrastructure for complete control and privacy. Kupmios combines Kupo (UTxO indexing) and Ogmios (node queries) for a fully local setup. Ideal for production applications that need guaranteed uptime and don't want external dependencies. ```ts twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, preprod, blockfrost, kupmios, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", +const sdkClient = client(preprod) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - }, - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 - } -}); +})); ``` ## Choosing a Provider @@ -79,37 +71,29 @@ Use Blockfrost for rapid prototyping and development. Switch to Kupmios when you The beauty of Evolution SDK's provider abstraction: your transaction code doesn't change. Only the configuration does: ```ts twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, preprod, blockfrost, kupmios, seedWallet } from "@evolution-sdk/evolution"; // Start with Blockfrost -const blockfrostClient = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const blockfrostClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: "key1" - }, - wallet: { - type: "seed", - mnemonic: "", +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 - } -}); +})); // Switch to Kupmios -const kupmiosClient = createClient({ - network: "preprod", - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", +const kupmiosClient = client(preprod) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - }, - wallet: { - type: "seed", - mnemonic: "", +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 - } -}); +})); ``` ## Environment Configuration @@ -126,29 +110,27 @@ OGMIOS_URL=http://localhost:1337 Then in your code: ```ts twoslash -import { createClient } from "@evolution-sdk/evolution"; - -const provider = process.env.BLOCKFROST_API_KEY - ? { - type: "blockfrost" as const, - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", - projectId: process.env.BLOCKFROST_API_KEY - } - : { - type: "kupmios" as const, - kupoUrl: process.env.KUPO_URL!, - ogmiosUrl: process.env.OGMIOS_URL! - }; - -const client = createClient({ - network: "preprod", - provider, - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, - accountIndex: 0 - } -}); +import { client, preprod, blockfrost, kupmios, seedWallet } from "@evolution-sdk/evolution"; + +const sdkClient = process.env.BLOCKFROST_API_KEY + ? client(preprod) + .with(blockfrost({ + baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", + projectId: process.env.BLOCKFROST_API_KEY + })) + .with(seedWallet({ + mnemonic: process.env.WALLET_MNEMONIC!, + accountIndex: 0 + })) + : client(preprod) + .with(kupmios({ + kupoUrl: process.env.KUPO_URL!, + ogmiosUrl: process.env.OGMIOS_URL! + })) + .with(seedWallet({ + mnemonic: process.env.WALLET_MNEMONIC!, + accountIndex: 0 + })); ``` ## Next Steps diff --git a/docs/content/docs/devnet/configuration.mdx b/docs/content/docs/devnet/configuration.mdx index 327ce5be..3682f13f 100644 --- a/docs/content/docs/devnet/configuration.mdx +++ b/docs/content/docs/devnet/configuration.mdx @@ -23,20 +23,17 @@ Addresses must be provided in hexadecimal format. Convert Bech32 addresses using ```typescript twoslash import { Cluster, Config } from "@evolution-sdk/devnet"; -import { Address, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, TransactionHash, client, seedWallet } from "@evolution-sdk/evolution"; // Create a client to generate an address -const client = createClient({ - network: 0, // Network magic 0 for custom devnet - wallet: { - type: "seed", - mnemonic: "your twenty-four word mnemonic phrase here", +const sdkClient = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic: "your twenty-four word mnemonic phrase here", accountIndex: 0 - } -}); +})); // Get the address -const address = await client.address(); +const address = await sdkClient.getAddress(); // Convert to hex for genesis configuration const addressHex = Address.toHex(address); @@ -71,28 +68,22 @@ Fund multiple addresses by adding additional entries to `initialFunds`. Each add ```typescript twoslash import { Cluster, Config } from "@evolution-sdk/devnet"; -import { Address, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, TransactionHash, client, seedWallet } from "@evolution-sdk/evolution"; -const client1 = createClient({ - network: 0, - wallet: { - type: "seed", - mnemonic: "your mnemonic here", +const client1 = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic: "your mnemonic here", accountIndex: 0 - } -}); +})); -const client2 = createClient({ - network: 0, - wallet: { - type: "seed", - mnemonic: "your mnemonic here", +const client2 = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic: "your mnemonic here", accountIndex: 1 // Different account index = different address - } -}); +})); -const address1 = Address.toHex(await client1.address()); -const address2 = Address.toHex(await client2.address()); +const address1 = Address.toHex(await client1.getAddress()); +const address2 = Address.toHex(await client2.getAddress()); const genesisConfig = { ...Config.DEFAULT_SHELLEY_GENESIS, @@ -121,7 +112,7 @@ After creating a genesis configuration, calculate the resulting UTxOs to verify ```typescript twoslash import { Config, Genesis } from "@evolution-sdk/devnet"; -import { Address, TransactionHash } from "@evolution-sdk/evolution"; +import { Address, TransactionHash, seedWallet, client } from "@evolution-sdk/evolution"; declare const addressHex: string; const genesisConfig = { @@ -304,29 +295,23 @@ Combining all customization options for a comprehensive devnet: ```typescript twoslash import { Cluster, Config, Genesis } from "@evolution-sdk/devnet"; -import { Address, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, TransactionHash, client, seedWallet } from "@evolution-sdk/evolution"; // Generate funded addresses -const wallet1 = createClient({ - network: 0, - wallet: { - type: "seed", - mnemonic: "test wallet one mnemonic here", +const wallet1 = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic: "test wallet one mnemonic here", accountIndex: 0 - } -}); +})); -const wallet2 = createClient({ - network: 0, - wallet: { - type: "seed", - mnemonic: "test wallet two mnemonic here", +const wallet2 = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic: "test wallet two mnemonic here", accountIndex: 0 - } -}); +})); -const addr1 = Address.toHex(await wallet1.address()); -const addr2 = Address.toHex(await wallet2.address()); +const addr1 = Address.toHex(await wallet1.getAddress()); +const addr2 = Address.toHex(await wallet2.getAddress()); // Custom genesis configuration const genesisConfig = { @@ -460,6 +445,6 @@ With custom genesis configuration, you can now: **Protocol parameter validation**: Some parameter combinations are invalid (e.g., `lovelacePerUTxOWord` too low in Alonzo genesis). Check cardano-node logs if startup fails. -**Client network parameter**: Always use `network: 0` for devnet clients to create testnet-format addresses (addr_test...). +**Client chain parameter**: Always use `Cluster.BOOTSTRAP_CHAIN` for devnet clients to create testnet-format addresses (addr_test...). **Excessive funds**: While genesis supports arbitrary amounts, extremely large values may cause numeric overflow in some calculations. Stay under 45B ADA (total supply) per address. diff --git a/docs/content/docs/devnet/integration.mdx b/docs/content/docs/devnet/integration.mdx index ad2b7973..517120c1 100644 --- a/docs/content/docs/devnet/integration.mdx +++ b/docs/content/docs/devnet/integration.mdx @@ -28,18 +28,18 @@ This example shows the full cycle from cluster creation to confirmed transaction ```typescript twoslash import { Cluster, Config, Genesis } from "@evolution-sdk/devnet"; -import { Address, Assets, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, TransactionHash, client, kupmios, seedWallet } from "@evolution-sdk/evolution"; const MNEMONIC = "your twenty-four word mnemonic phrase here"; async function completeWorkflow() { // Step 1: Generate sender address - const wallet = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: MNEMONIC, accountIndex: 0 } - }); + const wallet = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic: MNEMONIC, accountIndex: 0 +})); - const senderAddress = await wallet.address(); + const senderAddress = await wallet.getAddress(); const senderAddressHex = Address.toHex(senderAddress); console.log("Sender address:", senderAddress); @@ -71,20 +71,19 @@ async function completeWorkflow() { console.log("Devnet started with Kupo and Ogmios"); // Step 4: Create client connected to devnet - const client = createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", + const sdkClient = client(Cluster.getChain(cluster)) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - }, - wallet: { type: "seed", mnemonic: MNEMONIC, accountIndex: 0 } - }); +})) + .with(seedWallet({ +mnemonic: MNEMONIC, accountIndex: 0 +})); console.log("Client connected to devnet"); // Step 5: Query protocol parameters - const params = await client.getProtocolParameters(); + const params = await sdkClient.getProtocolParameters(); console.log("Protocol parameters:", { minFeeA: params.minFeeA, minFeeB: params.minFeeB, @@ -102,7 +101,7 @@ async function completeWorkflow() { "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae" ); - const signBuilder = await client + const signBuilder = await sdkClient .newTx() .payToAddress({ address: receiverAddress, @@ -125,11 +124,11 @@ async function completeWorkflow() { console.log("Transaction submitted:", txHash); // Step 10: Wait for confirmation - const confirmed = await client.awaitTx(txHash, 1000); + const confirmed = await sdkClient.awaitTx(txHash, 1000); console.log("Transaction confirmed:", confirmed); // Step 11: Query final state - const finalUtxos = await client.getWalletUtxos(); + const finalUtxos = await sdkClient.getWalletUtxos(); const totalBalance = finalUtxos.reduce( (sum, utxo) => sum + utxo.assets.lovelace, 0n @@ -169,7 +168,7 @@ Not all workflows require a wallet. Query blockchain state using a provider-only ```typescript twoslash import { Cluster, Config, Genesis } from "@evolution-sdk/devnet"; -import { Address, Assets, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, TransactionHash, client, kupmios, seedWallet } from "@evolution-sdk/evolution"; // Start devnet with funded address const cluster = await Cluster.make({ @@ -189,14 +188,11 @@ await Cluster.start(cluster); await new Promise(resolve => setTimeout(resolve, 6000)); // Provider-only client (no wallet configured) -const providerClient = createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", +const providerClient = client(Cluster.getChain(cluster)) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - } -}); +})); // Query any address on the devnet const utxos = await providerClient.getUtxos( @@ -221,22 +217,22 @@ Provider-only clients are ideal for blockchain explorers, monitoring tools, and ```typescript twoslash import { describe, it, beforeAll, afterAll, expect } from "vitest"; import { Cluster, Config } from "@evolution-sdk/devnet"; -import { Address, Assets, TransactionHash, createClient, type SigningClient } from "@evolution-sdk/evolution"; +import { Address, Assets, TransactionHash, client, kupmios, seedWallet } from "@evolution-sdk/evolution"; describe("Transaction Tests", () => { let cluster: Cluster.Cluster; - let client: SigningClient; + let sdkClient: any; beforeAll(async () => { // Setup devnet for test suite const mnemonic = "test test test test test test test test test test test test test test test test test test test test test test test sauce"; - const wallet = createClient({ - network: 0, - wallet: { type: "seed", mnemonic, accountIndex: 0 } - }); + const wallet = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic, accountIndex: 0 +})); - const addressHex = Address.toHex(await wallet.address()); + const addressHex = Address.toHex(await wallet.getAddress()); const genesisConfig = { ...Config.DEFAULT_SHELLEY_GENESIS, @@ -256,15 +252,14 @@ describe("Transaction Tests", () => { await Cluster.start(cluster); await new Promise(resolve => setTimeout(resolve, 8000)); - client = createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", + sdkClient = client(Cluster.getChain(cluster)) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - }, - wallet: { type: "seed", mnemonic, accountIndex: 0 } - }); +})) + .with(seedWallet({ +mnemonic, accountIndex: 0 +})); }, 180_000); // Extended timeout for cluster startup afterAll(async () => { @@ -273,7 +268,7 @@ describe("Transaction Tests", () => { }, 60_000); it("should submit simple payment", async () => { - const signBuilder = await client + const signBuilder = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae"), @@ -283,13 +278,13 @@ describe("Transaction Tests", () => { const submitBuilder = await signBuilder.sign(); const txHash = await submitBuilder.submit(); - const confirmed = await client.awaitTx(txHash, 1000); + const confirmed = await sdkClient.awaitTx(txHash, 1000); expect(confirmed).toBe(true); }, 30_000); it("should handle multiple outputs", async () => { - const signBuilder = await client + const signBuilder = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae"), @@ -303,7 +298,7 @@ describe("Transaction Tests", () => { const submitBuilder = await signBuilder.sign(); const txHash = await submitBuilder.submit(); - const confirmed = await client.awaitTx(txHash, 1000); + const confirmed = await sdkClient.awaitTx(txHash, 1000); expect(confirmed).toBe(true); }, 30_000); @@ -318,31 +313,25 @@ Test multi-party protocols by funding multiple addresses in genesis: ```typescript twoslash import { Cluster, Config } from "@evolution-sdk/devnet"; -import { Address, Assets, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, TransactionHash, client, kupmios, seedWallet } from "@evolution-sdk/evolution"; async function multiWalletExample() { // Create two wallets - const wallet1 = createClient({ - network: 0, - wallet: { - type: "seed", - mnemonic: "wallet one mnemonic here", + const wallet1 = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic: "wallet one mnemonic here", accountIndex: 0 - } - }); +})); - const wallet2 = createClient({ - network: 0, - wallet: { - type: "seed", - mnemonic: "wallet two mnemonic here", + const wallet2 = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic: "wallet two mnemonic here", accountIndex: 0 - } - }); +})); // Get addresses as hex for genesis config - const addr1Hex = Address.toHex(await wallet1.address()); - const addr2Hex = Address.toHex(await wallet2.address()); + const addr1Hex = Address.toHex(await wallet1.getAddress()); + const addr2Hex = Address.toHex(await wallet2.getAddress()); // Fund both in genesis const genesisConfig = { @@ -366,36 +355,28 @@ await Cluster.start(cluster); await new Promise(resolve => setTimeout(resolve, 8000)); // Create connected clients for both wallets -const client1 = createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", +const client1 = client(Cluster.getChain(cluster)) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - }, - wallet: { - type: "seed", - mnemonic: "wallet one mnemonic here", +})) + .with(seedWallet({ +mnemonic: "wallet one mnemonic here", accountIndex: 0 - } -}); +})); -const client2 = createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", +const client2 = client(Cluster.getChain(cluster)) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - }, - wallet: { - type: "seed", - mnemonic: "wallet two mnemonic here", +})) + .with(seedWallet({ +mnemonic: "wallet two mnemonic here", accountIndex: 0 - } -}); +})); // Wallet 1 sends to Wallet 2 - const wallet2Address = await wallet2.address(); + const wallet2Address = await wallet2.getAddress(); const signBuilder = await client1 .newTx() .payToAddress({ @@ -478,9 +459,9 @@ Run this script to start a persistent devnet for manual testing, dApp developmen **"UTxO not found"**: Remember that genesis UTxOs are NOT indexed by Kupo. Always use `calculateUtxosFromConfig` and provide them via `availableUtxos` parameter when building your first transaction. After spending genesis UTxOs, subsequent outputs will be indexed normally. -**"Transaction submission failed"**: Verify network magic matches between genesis configuration and client. Mismatched magic causes address validation failures. +**"Transaction submission failed"**: Verify network magic matches between genesis configuration and sdkClient. Mismatched magic causes address validation failures. -**"Fee estimation error"**: Ensure protocol parameters are accessible through the provider. Query `client.getProtocolParameters()` before building transactions to verify connectivity. +**"Fee estimation error"**: Ensure protocol parameters are accessible through the provider. Query `sdkClient.getProtocolParameters()` before building transactions to verify connectivity. **Port conflicts**: If Kupo/Ogmios won't start, check for port conflicts. Use different ports or stop conflicting services. diff --git a/docs/content/docs/governance/drep-registration.mdx b/docs/content/docs/governance/drep-registration.mdx index c1d7c068..894ab2fa 100644 --- a/docs/content/docs/governance/drep-registration.mdx +++ b/docs/content/docs/governance/drep-registration.mdx @@ -10,17 +10,19 @@ Delegated Representatives (DReps) vote on governance actions on behalf of ADA ho ## Register as a DRep ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const drepCredential: Credential.Credential -const tx = await client +const tx = await sdkClient .newTx() .registerDRep({ drepCredential }) .build() @@ -36,18 +38,20 @@ The deposit amount is fetched automatically from protocol parameters (`drepDepos Attach an anchor containing a metadata URL and its hash: ```typescript twoslash -import { Anchor, Credential, createClient } from "@evolution-sdk/evolution" +import { Anchor, Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const drepCredential: Credential.Credential declare const anchor: Anchor.Anchor -const tx = await client +const tx = await sdkClient .newTx() .registerDRep({ drepCredential, @@ -64,18 +68,20 @@ await signed.submit() Change the metadata anchor for an existing DRep registration: ```typescript twoslash -import { Anchor, Credential, createClient } from "@evolution-sdk/evolution" +import { Anchor, Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const drepCredential: Credential.Credential declare const newAnchor: Anchor.Anchor -const tx = await client +const tx = await sdkClient .newTx() .updateDRep({ drepCredential, @@ -92,17 +98,19 @@ await signed.submit() Remove DRep registration and reclaim the deposit: ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const drepCredential: Credential.Credential -const tx = await client +const tx = await sdkClient .newTx() .deregisterDRep({ drepCredential }) .build() @@ -118,18 +126,20 @@ await signed.submit() Committee members keep their cold credential offline and authorize a hot credential for day-to-day signing: ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const coldCredential: Credential.Credential declare const hotCredential: Credential.Credential -const tx = await client +const tx = await sdkClient .newTx() .authCommitteeHot({ coldCredential, hotCredential }) .build() @@ -141,18 +151,20 @@ await signed.submit() ### Resign from Committee ```typescript twoslash -import { Anchor, Credential, createClient } from "@evolution-sdk/evolution" +import { Anchor, Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const coldCredential: Credential.Credential declare const resignationAnchor: Anchor.Anchor -const tx = await client +const tx = await sdkClient .newTx() .resignCommitteeCold({ coldCredential, diff --git a/docs/content/docs/governance/index.mdx b/docs/content/docs/governance/index.mdx index 307f1e49..070ea4b5 100644 --- a/docs/content/docs/governance/index.mdx +++ b/docs/content/docs/governance/index.mdx @@ -21,18 +21,20 @@ Evolution SDK supports Cardano's Conway-era governance system (CIP-1694). Regist ## Quick Example ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const drepCredential: Credential.Credential // Register as a DRep -const tx = await client +const tx = await sdkClient .newTx() .registerDRep({ drepCredential }) .build() @@ -46,18 +48,20 @@ await signed.submit() All governance operations support script-controlled credentials. Provide a redeemer when the credential is a script hash: ```typescript twoslash -import { Credential, Data, createClient } from "@evolution-sdk/evolution" +import { Credential, Data, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptDrepCredential: Credential.Credential declare const governanceScript: any -const tx = await client +const tx = await sdkClient .newTx() .registerDRep({ drepCredential: scriptDrepCredential, diff --git a/docs/content/docs/governance/proposals.mdx b/docs/content/docs/governance/proposals.mdx index 00deb809..043dd20e 100644 --- a/docs/content/docs/governance/proposals.mdx +++ b/docs/content/docs/governance/proposals.mdx @@ -10,19 +10,21 @@ Governance proposals submit actions for the community to vote on. The deposit is ## Submitting a Proposal ```typescript twoslash -import { Anchor, GovernanceAction, RewardAccount, createClient } from "@evolution-sdk/evolution" +import { Anchor, GovernanceAction, RewardAccount, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const governanceAction: GovernanceAction.GovernanceAction declare const rewardAccount: RewardAccount.RewardAccount declare const anchor: Anchor.Anchor -const tx = await client +const tx = await sdkClient .newTx() .propose({ governanceAction, @@ -54,13 +56,15 @@ The `govActionDeposit` is deducted automatically during transaction balancing. Submit multiple proposals in a single transaction by chaining `.propose()`: ```typescript twoslash -import { Anchor, GovernanceAction, RewardAccount, createClient } from "@evolution-sdk/evolution" +import { Anchor, GovernanceAction, RewardAccount, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const action1: GovernanceAction.GovernanceAction declare const action2: GovernanceAction.GovernanceAction @@ -68,7 +72,7 @@ declare const rewardAccount: RewardAccount.RewardAccount declare const anchor1: Anchor.Anchor declare const anchor2: Anchor.Anchor -const tx = await client +const tx = await sdkClient .newTx() .propose({ governanceAction: action1, rewardAccount, anchor: anchor1 }) .propose({ governanceAction: action2, rewardAccount, anchor: anchor2 }) diff --git a/docs/content/docs/governance/vote-delegation.mdx b/docs/content/docs/governance/vote-delegation.mdx index 61ad6246..c9213418 100644 --- a/docs/content/docs/governance/vote-delegation.mdx +++ b/docs/content/docs/governance/vote-delegation.mdx @@ -10,18 +10,20 @@ In the Conway era, ADA holders can delegate their governance voting power to a D ## Delegate to a DRep ```typescript twoslash -import { Credential, DRep, createClient } from "@evolution-sdk/evolution" +import { Credential, DRep, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const drepKeyHash: any -const tx = await client +const tx = await sdkClient .newTx() .delegateToDRep({ stakeCredential, @@ -43,18 +45,20 @@ Instead of delegating to a specific DRep, you can choose a built-in option: | **AlwaysNoConfidence** | Your power always counts as "no confidence" in the committee | ```typescript twoslash -import { Credential, DRep, createClient } from "@evolution-sdk/evolution" +import { Credential, DRep, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential // Abstain from all governance votes -const tx = await client +const tx = await sdkClient .newTx() .delegateToDRep({ stakeCredential, @@ -71,19 +75,21 @@ await signed.submit() Use `delegateToPoolAndDRep` to delegate both in a single certificate: ```typescript twoslash -import { Credential, DRep, createClient } from "@evolution-sdk/evolution" +import { Credential, DRep, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const poolKeyHash: any declare const drepKeyHash: any -const tx = await client +const tx = await sdkClient .newTx() .delegateToPoolAndDRep({ stakeCredential, @@ -101,18 +107,20 @@ await signed.submit() For new stake credentials, combine registration with vote delegation: ```typescript twoslash -import { Credential, DRep, createClient } from "@evolution-sdk/evolution" +import { Credential, DRep, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const drepKeyHash: any -const tx = await client +const tx = await sdkClient .newTx() .registerAndDelegateTo({ stakeCredential, diff --git a/docs/content/docs/governance/voting.mdx b/docs/content/docs/governance/voting.mdx index aed35285..e1728b56 100644 --- a/docs/content/docs/governance/voting.mdx +++ b/docs/content/docs/governance/voting.mdx @@ -10,17 +10,19 @@ DReps, Constitutional Committee members, and Stake Pool Operators can vote on go ## Casting a Vote ```typescript twoslash -import { VotingProcedures, createClient } from "@evolution-sdk/evolution" +import { VotingProcedures, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const votingProcedures: VotingProcedures.VotingProcedures -const tx = await client +const tx = await sdkClient .newTx() .vote({ votingProcedures }) .build() @@ -42,18 +44,20 @@ await signed.submit() For DReps or Committee members with script credentials, provide a redeemer: ```typescript twoslash -import { Data, VotingProcedures, createClient } from "@evolution-sdk/evolution" +import { Data, VotingProcedures, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const votingProcedures: VotingProcedures.VotingProcedures declare const votingScript: any -const tx = await client +const tx = await sdkClient .newTx() .vote({ votingProcedures, diff --git a/docs/content/docs/introduction/getting-started.mdx b/docs/content/docs/introduction/getting-started.mdx index 9c8350da..96176b82 100644 --- a/docs/content/docs/introduction/getting-started.mdx +++ b/docs/content/docs/introduction/getting-started.mdx @@ -27,16 +27,13 @@ npm install @evolution-sdk/evolution ### 2. Create a Wallet Instantiate a wallet using your seed phrase or private keys: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - wallet: { - type: "seed", - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +const sdkClient = client(preprod) + .with(seedWallet({ +mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", accountIndex: 0 - } -}); +})); ``` See [Creating Wallets](/docs/wallets) for all wallet types. @@ -44,21 +41,17 @@ See [Creating Wallets](/docs/wallets) for all wallet types. ### 3. Attach a Provider Connect to the blockchain via a provider: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +})) + .with(seedWallet({ +mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", accountIndex: 0 - } -}); +})); ``` Learn more in [Clients](/docs/clients) and [Providers](/docs/clients/providers). @@ -66,24 +59,20 @@ Learn more in [Clients](/docs/clients) and [Providers](/docs/clients/providers). ### 4. Build a Transaction Construct your first payment: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: "your-project-id" - }, - wallet: { - type: "seed", - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +})) + .with(seedWallet({ +mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", accountIndex: 0 - } -}); +})); // ---cut--- -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1qzrf9g3ea6hdc5vfujgrpjc0c0xq3qqkz8zkpwh3s6nqzhgey8k3eq73kr0gcqd7cyy75s0qqx0qqx0qqx0qqx0qx7e8pq"), @@ -97,23 +86,19 @@ Details in [Transactions](/docs/transactions). ### 5. Sign & Submit Sign with your wallet and send to the network: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: "your-project-id" - }, - wallet: { - type: "seed", - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +})) + .with(seedWallet({ +mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", accountIndex: 0 - } -}); +})); -const tx = await client.newTx() +const tx = await sdkClient.newTx() .payToAddress({ address: Address.fromBech32("addr_test1qzrf9g3ea6hdc5vfujgrpjc0c0xq3qqkz8zkpwh3s6nqzhgey8k3eq73kr0gcqd7cyy75s0qqx0qqx0qqx0qqx0qx7e8pq"), assets: Assets.fromLovelace(2_000_000n) diff --git a/docs/content/docs/introduction/installation.mdx b/docs/content/docs/introduction/installation.mdx index 185743c9..3337d5c9 100644 --- a/docs/content/docs/introduction/installation.mdx +++ b/docs/content/docs/introduction/installation.mdx @@ -56,21 +56,17 @@ Most modern frameworks (React, Vue, Svelte) bundle Evolution SDK without issues. Test that everything is working: ```ts twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: "your-project-id" - }, - wallet: { - type: "seed", - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +})) + .with(seedWallet({ +mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", accountIndex: 0 - } -}); +})); console.log("Evolution SDK loaded successfully!"); ``` diff --git a/docs/content/docs/introduction/migration-from-lucid.mdx b/docs/content/docs/introduction/migration-from-lucid.mdx index c54ea01a..cb827257 100644 --- a/docs/content/docs/introduction/migration-from-lucid.mdx +++ b/docs/content/docs/introduction/migration-from-lucid.mdx @@ -36,21 +36,17 @@ const lucid = await Lucid.new(blockfrostProvider, "Preprod"); **Evolution SDK:** ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: "your-project-id" - }, - wallet: { - type: "seed", - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +})) + .with(seedWallet({ +mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", accountIndex: 0 - } -}); +})); ``` ### Building Transactions @@ -65,24 +61,20 @@ lucid **Evolution SDK:** ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: "your-project-id" - }, - wallet: { - type: "seed", - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", +})) + .with(seedWallet({ +mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", accountIndex: 0 - } -}); +})); // ---cut--- -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1qzrf9g3ea6hdc5vfujgrpjc0c0xq3qqkz8zkpwh3s6nqzhgey8k3eq73kr0gcqd7cyy75s0qqx0qqx0qqx0qqx0qx7e8pq"), @@ -97,7 +89,7 @@ Here are the typical changes you'll make throughout your codebase: | Lucid | Evolution SDK | |-------|---------------| -| `Lucid.new(provider, network)` | `createClient({ network, provider, wallet })` | +| `Lucid.new(provider, network)` | `client(chain).with(provider).with(wallet)` | | `.payToAddress(addr, assets)` | `.payToAddress({ address, assets })` | | `.complete()` | `.build()` | | `.signWithWallet()` | `tx.sign()` | diff --git a/docs/content/docs/modules/CBOR.mdx b/docs/content/docs/modules/CBOR.mdx index f42af70b..2d0ae087 100644 --- a/docs/content/docs/modules/CBOR.mdx +++ b/docs/content/docs/modules/CBOR.mdx @@ -20,17 +20,41 @@ parent: Modules - [CML_DATA_DEFAULT_OPTIONS](#cml_data_default_options) - [CML_DEFAULT_OPTIONS](#cml_default_options) - [STRUCT_FRIENDLY_OPTIONS](#struct_friendly_options) +- [decoding](#decoding) + - [decodeItemWithOffset](#decodeitemwithoffset) - [encoding](#encoding) - [toCBORBytes](#tocborbytes) + - [toCBORBytesWithFormat](#tocborbyteswithformat) - [toCBORHex](#tocborhex) + - [toCBORHexWithFormat](#tocborhexwithformat) +- [equality](#equality) + - [equals](#equals) - [errors](#errors) - [CBORError (class)](#cborerror-class) - [model](#model) + - [BoundedBytes](#boundedbytes) + - [ByteSize (type alias)](#bytesize-type-alias) - [CBOR (type alias)](#cbor-type-alias) + - [CBORFormat (type alias)](#cborformat-type-alias) + - [CBORFormat (namespace)](#cborformat-namespace) + - [Array (type alias)](#array-type-alias) + - [Bytes (type alias)](#bytes-type-alias) + - [Map (type alias)](#map-type-alias) + - [NInt (type alias)](#nint-type-alias) + - [Simple (type alias)](#simple-type-alias) + - [Tag (type alias)](#tag-type-alias) + - [Text (type alias)](#text-type-alias) + - [UInt (type alias)](#uint-type-alias) - [CodecOptions (type alias)](#codecoptions-type-alias) + - [DecodedWithFormat (type alias)](#decodedwithformat-type-alias) + - [LengthEncoding (type alias)](#lengthencoding-type-alias) + - [StringEncoding (type alias)](#stringencoding-type-alias) - [parsing](#parsing) - [fromCBORBytes](#fromcborbytes) + - [fromCBORBytesWithFormat](#fromcborbyteswithformat) - [fromCBORHex](#fromcborhex) + - [fromCBORHexWithFormat](#fromcborhexwithformat) + - [internalDecodeWithFormatSync](#internaldecodewithformatsync) - [schemas](#schemas) - [CBORSchema](#cborschema) - [FromBytes](#frombytes) @@ -68,14 +92,17 @@ parent: Modules ## AIKEN_DEFAULT_OPTIONS -Aiken-compatible CBOR encoding options +Aiken-compatible CBOR encoding options. -Matches the encoding used by Aiken's cbor.serialise(): +Matches the encoding produced by `cbor.serialise()` in Aiken: -- Indefinite-length arrays (9f...ff) +- Indefinite-length arrays (`9f...ff`) - Maps encoded as arrays of pairs (not CBOR maps) -- Strings as bytearrays (major type 2, not 3) -- Constructor tags: 121-127 for indices 0-6, then 1280+ for 7+ +- Strings as byte arrays (major type 2, not 3) +- Constructor tags: 121–127 for indices 0–6, then 1280+ for 7+ + +PlutusData byte strings are chunked per the Conway `bounded_bytes` rule +via the `BoundedBytes` CBOR node, independent of these codec options. **Signature** @@ -169,7 +196,11 @@ Added in v1.0.0 ## CML_DATA_DEFAULT_OPTIONS -Default CBOR encoding option for Data +Default CBOR encoding options for PlutusData. + +Uses indefinite-length arrays and maps. The `bounded_bytes` constraint +(Conway CDDL: byte strings ≤ 64 bytes) is enforced at the data-type layer +via the `BoundedBytes` CBOR node, independent of these codec options. **Signature** @@ -203,6 +234,25 @@ export declare const STRUCT_FRIENDLY_OPTIONS: CodecOptions Added in v2.0.0 +# decoding + +## decodeItemWithOffset + +Decode a single CBOR item at a given byte offset, returning the decoded value and the new offset. +Useful for extracting raw byte slices from CBOR-encoded data without re-encoding. + +**Signature** + +```ts +export declare const decodeItemWithOffset: ( + data: Uint8Array, + offset: number, + options?: CodecOptions +) => { item: CBOR; newOffset: number } +``` + +Added in v2.0.0 + # encoding ## toCBORBytes @@ -217,6 +267,18 @@ export declare const toCBORBytes: (value: CBOR, options?: CodecOptions) => Uint8 Added in v1.0.0 +## toCBORBytesWithFormat + +Convert a CBOR value to CBOR bytes using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORBytesWithFormat: (value: CBOR, format: CBORFormat) => Uint8Array +``` + +Added in v2.0.0 + ## toCBORHex Convert a CBOR value to CBOR hex string. @@ -229,6 +291,36 @@ export declare const toCBORHex: (value: CBOR, options?: CodecOptions) => string Added in v1.0.0 +## toCBORHexWithFormat + +Convert a CBOR value to CBOR hex string using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORHexWithFormat: (value: CBOR, format: CBORFormat) => string +``` + +Added in v2.0.0 + +# equality + +## equals + +Schema-derived structural equivalence for CBOR values. +Handles Uint8Array, Array, Map, Tag and all primitives via the +recursive CBORSchema definition — no hand-rolled comparison needed. + +Derived once at module init; at call time it's a plain function. + +**Signature** + +```ts +export declare const equals: (a: CBOR, b: CBOR) => boolean +``` + +Added in v2.0.0 + # errors ## CBORError (class) @@ -245,6 +337,41 @@ Added in v1.0.0 # model +## BoundedBytes + +`BoundedBytes` CBOR node — represents a PlutusData byte string that must comply +with the Conway CDDL constraint `bounded_bytes = bytes .size (0..64)`. + +The encoding rule is unconditional and options-independent: + +- ≤ 64 bytes → definite-length CBOR bytes +- > 64 bytes → indefinite-length 64-byte chunked byte string (`0x5f` + chunks + `0xff`) + +Use `BoundedBytes.make` to construct the node; the encoder handles the rest. + +**Signature** + +```ts +export declare const BoundedBytes: { + readonly make: (bytes: Uint8Array) => CBOR + readonly is: (value: CBOR) => value is { _tag: "BoundedBytes"; bytes: Uint8Array } +} +``` + +Added in v2.0.0 + +## ByteSize (type alias) + +Width of a CBOR integer argument: inline (0), 1-byte, 2-byte, 4-byte, or 8-byte. + +**Signature** + +```ts +export type ByteSize = 0 | 1 | 2 | 4 | 8 +``` + +Added in v2.0.0 + ## CBOR (type alias) Type representing a CBOR value with simplified, non-tagged structure @@ -263,11 +390,131 @@ export type CBOR = | boolean // boolean values | null // null value | undefined // undefined value - | number + | number // floating point numbers + | { _tag: "BoundedBytes"; bytes: Uint8Array } ``` Added in v1.0.0 +## CBORFormat (type alias) + +Tagged discriminated union capturing how each CBOR node was originally +serialized. Every variant carries a `_tag` discriminant. Encoding-detail +fields are optional — absent means "use canonical / minimal default". + +**Signature** + +```ts +export type CBORFormat = + | CBORFormat.UInt + | CBORFormat.NInt + | CBORFormat.Bytes + | CBORFormat.Text + | CBORFormat.Array + | CBORFormat.Map + | CBORFormat.Tag + | CBORFormat.Simple +``` + +Added in v2.0.0 + +## CBORFormat (namespace) + +Added in v2.0.0 + +### Array (type alias) + +Array (major 4). `length` absent → definite, minimal length header. + +**Signature** + +```ts +export type Array = { + readonly _tag: "array" + readonly length?: LengthEncoding + readonly children: ReadonlyArray +} +``` + +### Bytes (type alias) + +Byte string (major 2). `encoding` absent → definite, minimal length. + +**Signature** + +```ts +export type Bytes = { readonly _tag: "bytes"; readonly encoding?: StringEncoding } +``` + +### Map (type alias) + +Map (major 5). `keyOrder` stores CBOR-encoded key bytes for serializable ordering. + +**Signature** + +```ts +export type Map = { + readonly _tag: "map" + readonly length?: LengthEncoding + readonly keyOrder?: ReadonlyArray + readonly entries: ReadonlyArray +} +``` + +### NInt (type alias) + +Negative integer (major 1). `byteSize` absent → minimal encoding. + +**Signature** + +```ts +export type NInt = { readonly _tag: "nint"; readonly byteSize?: ByteSize } +``` + +### Simple (type alias) + +Simple value or float (major 7). No encoding choices to preserve. + +**Signature** + +```ts +export type Simple = { readonly _tag: "simple" } +``` + +### Tag (type alias) + +Tag (major 6). `width` absent → minimal tag header. + +**Signature** + +```ts +export type Tag = { + readonly _tag: "tag" + readonly width?: ByteSize + readonly child: CBORFormat +} +``` + +### Text (type alias) + +Text string (major 3). `encoding` absent → definite, minimal length. + +**Signature** + +```ts +export type Text = { readonly _tag: "text"; readonly encoding?: StringEncoding } +``` + +### UInt (type alias) + +Unsigned integer (major 0). `byteSize` absent → minimal encoding. + +**Signature** + +```ts +export type UInt = { readonly _tag: "uint"; readonly byteSize?: ByteSize } +``` + ## CodecOptions (type alias) CBOR codec configuration options @@ -295,6 +542,50 @@ export type CodecOptions = Added in v1.0.0 +## DecodedWithFormat (type alias) + +Decoded value paired with its captured root format tree. + +**Signature** + +```ts +export type DecodedWithFormat = { + value: A + format: CBORFormat +} +``` + +Added in v2.0.0 + +## LengthEncoding (type alias) + +Container length encoding style captured during decode. + +**Signature** + +```ts +export type LengthEncoding = { readonly tag: "indefinite" } | { readonly tag: "definite"; readonly byteSize: ByteSize } +``` + +Added in v2.0.0 + +## StringEncoding (type alias) + +Byte/text string encoding style captured during decode. + +**Signature** + +```ts +export type StringEncoding = + | { readonly tag: "definite"; readonly byteSize: ByteSize } + | { + readonly tag: "indefinite" + readonly chunks: ReadonlyArray<{ readonly length: number; readonly byteSize: ByteSize }> + } +``` + +Added in v2.0.0 + # parsing ## fromCBORBytes @@ -309,6 +600,18 @@ export declare const fromCBORBytes: (bytes: Uint8Array, options?: CodecOptions) Added in v1.0.0 +## fromCBORBytesWithFormat + +Parse a CBOR value from CBOR bytes and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORBytesWithFormat: (bytes: Uint8Array) => DecodedWithFormat +``` + +Added in v2.0.0 + ## fromCBORHex Parse a CBOR value from CBOR hex string. @@ -321,6 +624,30 @@ export declare const fromCBORHex: (hex: string, options?: CodecOptions) => CBOR Added in v1.0.0 +## fromCBORHexWithFormat + +Parse a CBOR value from CBOR hex string and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORHexWithFormat: (hex: string) => DecodedWithFormat +``` + +Added in v2.0.0 + +## internalDecodeWithFormatSync + +Decode CBOR bytes and return both the decoded value and the root format tree. + +**Signature** + +```ts +export declare const internalDecodeWithFormatSync: (data: Uint8Array) => DecodedWithFormat +``` + +Added in v2.0.0 + # schemas ## CBORSchema @@ -385,6 +712,7 @@ export declare const match: ( null: () => R undefined: () => R float: (value: number) => R + boundedBytes: (value: Uint8Array) => R } ) => R ``` @@ -524,7 +852,7 @@ export declare const internalDecodeSync: (data: Uint8Array, options?: CodecOptio **Signature** ```ts -export declare const internalEncodeSync: (value: CBOR, options?: CodecOptions) => Uint8Array +export declare const internalEncodeSync: (value: CBOR, options?: CodecOptions, fmt?: CBORFormat) => Uint8Array ``` ## isArray diff --git a/docs/content/docs/modules/Data.mdx b/docs/content/docs/modules/Data.mdx index d693e788..4dd2e1c6 100644 --- a/docs/content/docs/modules/Data.mdx +++ b/docs/content/docs/modules/Data.mdx @@ -180,8 +180,9 @@ Added in v2.0.0 ## equals -Deep structural equality for Plutus Data values. -Handles maps, lists, ints, bytes, and constrs. +Schema-derived structural equality for Plutus Data values. +Handles maps, lists, ints, bytes, and constrs via the +recursive DataSchema definition — no hand-rolled comparison needed. **Signature** diff --git a/docs/content/docs/modules/PrivateKey.mdx b/docs/content/docs/modules/PrivateKey.mdx index dc82ab72..86ddb65f 100644 --- a/docs/content/docs/modules/PrivateKey.mdx +++ b/docs/content/docs/modules/PrivateKey.mdx @@ -13,13 +13,14 @@ parent: Modules - [arbitrary](#arbitrary) - [arbitrary](#arbitrary-1) - [bip32](#bip32) - - [derive](#derive) + - [~~derive~~](#derive) - [bip39](#bip39) - - [fromMnemonic](#frommnemonic) + - [~~fromMnemonic~~](#frommnemonic) - [generateMnemonic](#generatemnemonic) - [validateMnemonic](#validatemnemonic) - [cardano](#cardano) - - [CardanoPath](#cardanopath) + - [~~CardanoPath~~](#cardanopath) + - [fromMnemonicCardano](#frommnemoniccardano) - [cryptography](#cryptography) - [sign](#sign) - [toPublicKey](#topublickey) @@ -70,10 +71,12 @@ Added in v2.0.0 # bip32 -## derive +## ~~derive~~ Derive a child private key using BIP32 path (sync version that throws PrivateKeyError). -All errors are normalized to PrivateKeyError with contextual information. + +**WARNING**: This uses secp256k1 BIP32 derivation (`@scure/bip32`), NOT Cardano's +BIP32-Ed25519. For Cardano key derivation, use `fromMnemonicCardano` instead. **Signature** @@ -85,10 +88,12 @@ Added in v2.0.0 # bip39 -## fromMnemonic +## ~~fromMnemonic~~ Create a PrivateKey from a mnemonic phrase (sync version that throws PrivateKeyError). -All errors are normalized to PrivateKeyError with contextual information. + +**WARNING**: This uses secp256k1 BIP32 derivation (`@scure/bip32`), NOT Cardano's +BIP32-Ed25519. For Cardano key derivation, use `fromMnemonicCardano` instead. **Signature** @@ -124,10 +129,15 @@ Added in v2.0.0 # cardano -## CardanoPath +## ~~CardanoPath~~ Cardano BIP44 derivation path utilities. +**WARNING**: These paths are only useful with BIP32-Ed25519 derivation +(`Bip32PrivateKey`). Using them with `derive` (which uses secp256k1 BIP32) +will produce incorrect keys. Use `fromMnemonicCardano` or +`Bip32PrivateKey.CardanoPath` instead. + **Signature** ```ts @@ -140,6 +150,41 @@ export declare const CardanoPath: { Added in v2.0.0 +## fromMnemonicCardano + +Derive a Cardano payment or stake key from a mnemonic using BIP32-Ed25519. + +This is the correct way to derive Cardano keys from a mnemonic. It uses the +Icarus/V2 BIP32-Ed25519 derivation scheme, matching CML and cardano-cli behavior. + +**Signature** + +```ts +export declare const fromMnemonicCardano: ( + mnemonic: string, + options?: { account?: number; role?: 0 | 2; index?: number; password?: string } +) => PrivateKey +``` + +**Example** + +```ts +import * as PrivateKey from "@evolution-sdk/evolution/PrivateKey" + +const mnemonic = PrivateKey.generateMnemonic() + +// Payment key (default: account 0, index 0) +const paymentKey = PrivateKey.fromMnemonicCardano(mnemonic) + +// Stake key +const stakeKey = PrivateKey.fromMnemonicCardano(mnemonic, { role: 2 }) + +// Custom account/index +const key = PrivateKey.fromMnemonicCardano(mnemonic, { account: 1, index: 3 }) +``` + +Added in v2.0.0 + # cryptography ## sign diff --git a/docs/content/docs/modules/Redeemers.mdx b/docs/content/docs/modules/Redeemers.mdx index 5a6f3892..5292b895 100644 --- a/docs/content/docs/modules/Redeemers.mdx +++ b/docs/content/docs/modules/Redeemers.mdx @@ -12,19 +12,34 @@ parent: Modules - [arbitrary](#arbitrary) - [arbitrary](#arbitrary-1) +- [constructors](#constructors) + - [makeRedeemerMap](#makeredeemermap) - [encoding](#encoding) - [toCBORBytes](#tocborbytes) - [toCBORBytesMap](#tocborbytesmap) - [toCBORHex](#tocborhex) - [toCBORHexMap](#tocborhexmap) - [model](#model) - - [Format (type alias)](#format-type-alias) - - [Redeemers (class)](#redeemers-class) + - [RedeemerArray (class)](#redeemerarray-class) + - [toArray (method)](#toarray-method) - [toJSON (method)](#tojson-method) - [toString (method)](#tostring-method) - [[Inspectable.NodeInspectSymbol] (method)](#inspectablenodeinspectsymbol-method) - [[Equal.symbol] (method)](#equalsymbol-method) - [[Hash.symbol] (method)](#hashsymbol-method) + - [RedeemerKey (type alias)](#redeemerkey-type-alias) + - [RedeemerMap (class)](#redeemermap-class) + - [get (method)](#get-method) + - [toArray (method)](#toarray-method-1) + - [toJSON (method)](#tojson-method-1) + - [toString (method)](#tostring-method-1) + - [[Inspectable.NodeInspectSymbol] (method)](#inspectablenodeinspectsymbol-method-1) + - [[Equal.symbol] (method)](#equalsymbol-method-1) + - [[Hash.symbol] (method)](#hashsymbol-method-1) + - [RedeemerValue (class)](#redeemervalue-class) + - [[Equal.symbol] (method)](#equalsymbol-method-2) + - [[Hash.symbol] (method)](#hashsymbol-method-2) + - [Redeemers (type alias)](#redeemers-type-alias) - [parsing](#parsing) - [fromCBORBytes](#fromcborbytes) - [fromCBORBytesMap](#fromcborbytesmap) @@ -41,6 +56,9 @@ parent: Modules - [FromCDDL](#fromcddl) - [FromMapCDDL](#frommapcddl) - [MapCDDLSchema](#mapcddlschema) + - [Redeemers](#redeemers) +- [utilities](#utilities) + - [keyToString](#keytostring) --- @@ -48,12 +66,26 @@ parent: Modules ## arbitrary -FastCheck arbitrary for Redeemers. +FastCheck arbitrary for Redeemers — generates both map and array variants. **Signature** ```ts -export declare const arbitrary: FastCheck.Arbitrary +export declare const arbitrary: FastCheck.Arbitrary +``` + +Added in v2.0.0 + +# constructors + +## makeRedeemerMap + +Create a `RedeemerMap` from an array of `Redeemer` objects. + +**Signature** + +```ts +export declare const makeRedeemerMap: (redeemers: ReadonlyArray) => RedeemerMap ``` Added in v2.0.0 @@ -62,90 +94,183 @@ Added in v2.0.0 ## toCBORBytes -Encode Redeemers to CBOR bytes (array format). +Encode to CBOR bytes (array format). **Signature** ```ts -export declare const toCBORBytes: (data: Redeemers, options?: CBOR.CodecOptions) => any +export declare const toCBORBytes: (data: RedeemerArray, options?: CBOR.CodecOptions) => any ``` Added in v2.0.0 ## toCBORBytesMap -Encode Redeemers to CBOR bytes (map format). +Encode to CBOR bytes (map format). **Signature** ```ts -export declare const toCBORBytesMap: (data: Redeemers, options?: CBOR.CodecOptions) => any +export declare const toCBORBytesMap: (data: RedeemerMap, options?: CBOR.CodecOptions) => any ``` Added in v2.0.0 ## toCBORHex -Encode Redeemers to CBOR hex string (array format). +Encode to CBOR hex string (array format). **Signature** ```ts -export declare const toCBORHex: (data: Redeemers, options?: CBOR.CodecOptions) => string +export declare const toCBORHex: (data: RedeemerArray, options?: CBOR.CodecOptions) => string ``` Added in v2.0.0 ## toCBORHexMap -Encode Redeemers to CBOR hex string (map format). +Encode to CBOR hex string (map format). **Signature** ```ts -export declare const toCBORHexMap: (data: Redeemers, options?: CBOR.CodecOptions) => string +export declare const toCBORHexMap: (data: RedeemerMap, options?: CBOR.CodecOptions) => string ``` Added in v2.0.0 # model -## Format (type alias) +## RedeemerArray (class) -Encoding format for redeemers collection. +Redeemers in legacy array format. -Conway CDDL supports two formats: +Mirrors the CDDL: ``` -; Flat Array support is included for backwards compatibility and -; will be removed in the next era. It is recommended for tools to -; adopt using a Map instead of Array going forward. -redeemers = - [ + redeemer ] - / { + [tag : redeemer_tag, index : uint .size 4] => [ data : plutus_data, ex_units : ex_units ] } +[ + redeemer ] ``` -- "array": Legacy flat array format - backwards compatible, will be deprecated -- "map": New map format - recommended for Conway+ +Backwards compatible — will be deprecated in the next era. +Prefer `RedeemerMap` for new transactions. **Signature** ```ts -export type Format = "array" | "map" +export declare class RedeemerArray ``` Added in v2.0.0 -## Redeemers (class) +### toArray (method) + +Convert to an array of `Redeemer` objects (identity for array format). -Redeemers collection based on Conway CDDL specification. +**Signature** + +```ts +toArray(): ReadonlyArray +``` -Represents a collection of redeemers that can be encoded in either array or map format. +Added in v2.0.0 + +### toJSON (method) **Signature** ```ts -export declare class Redeemers +toJSON() +``` + +### toString (method) + +**Signature** + +```ts +toString(): string +``` + +### [Inspectable.NodeInspectSymbol] (method) + +**Signature** + +```ts +[Inspectable.NodeInspectSymbol](): unknown +``` + +### [Equal.symbol] (method) + +**Signature** + +```ts +[Equal.symbol](that: unknown): boolean +``` + +### [Hash.symbol] (method) + +**Signature** + +```ts +[Hash.symbol](): number +``` + +## RedeemerKey (type alias) + +A redeemer map key: `[tag, index]`. + +Mirrors the CDDL: `[tag : redeemer_tag, index : uint .size 4]` + +**Signature** + +```ts +export type RedeemerKey = readonly [Redeemer.RedeemerTag, bigint] +``` + +Added in v2.0.0 + +## RedeemerMap (class) + +Redeemers in map format (Conway recommended). + +Mirrors the CDDL exactly: + +``` +{ + [tag : redeemer_tag, index : uint .size 4] => [ data : plutus_data, ex_units : ex_units ] } +``` + +The map is keyed by `[tag, index]` tuples. Note: JS Map uses reference +equality for non-primitive keys, so lookups by tuple won't work — use +`get()` or `toArray()` helpers instead. + +**Signature** + +```ts +export declare class RedeemerMap +``` + +Added in v2.0.0 + +### get (method) + +Look up a redeemer entry by tag and index. + +**Signature** + +```ts +get(tag: Redeemer.RedeemerTag, index: bigint): RedeemerValue | undefined +``` + +Added in v2.0.0 + +### toArray (method) + +Convert to an array of `Redeemer` objects (convenience for consumers). + +**Signature** + +```ts +toArray(): ReadonlyArray ``` Added in v2.0.0 @@ -190,52 +315,94 @@ toString(): string [Hash.symbol](): number ``` +## RedeemerValue (class) + +A redeemer map entry value: `[data, ex_units]`. + +Mirrors the CDDL: `[data : plutus_data, ex_units : ex_units]` + +**Signature** + +```ts +export declare class RedeemerValue +``` + +Added in v2.0.0 + +### [Equal.symbol] (method) + +**Signature** + +```ts +[Equal.symbol](that: unknown): boolean +``` + +### [Hash.symbol] (method) + +**Signature** + +```ts +[Hash.symbol](): number +``` + +## Redeemers (type alias) + +Union type: `RedeemerMap | RedeemerArray` + +**Signature** + +```ts +export type Redeemers = typeof Redeemers.Type +``` + +Added in v2.0.0 + # parsing ## fromCBORBytes -Parse Redeemers from CBOR bytes (array format). +Parse from CBOR bytes (array format). **Signature** ```ts -export declare const fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOptions) => Redeemers +export declare const fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOptions) => RedeemerArray ``` Added in v2.0.0 ## fromCBORBytesMap -Parse Redeemers from CBOR bytes (map format). +Parse from CBOR bytes (map format). **Signature** ```ts -export declare const fromCBORBytesMap: (bytes: Uint8Array, options?: CBOR.CodecOptions) => Redeemers +export declare const fromCBORBytesMap: (bytes: Uint8Array, options?: CBOR.CodecOptions) => RedeemerMap ``` Added in v2.0.0 ## fromCBORHex -Parse Redeemers from CBOR hex string (array format). +Parse from CBOR hex string (array format). **Signature** ```ts -export declare const fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => Redeemers +export declare const fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => RedeemerArray ``` Added in v2.0.0 ## fromCBORHexMap -Parse Redeemers from CBOR hex string (map format). +Parse from CBOR hex string (map format). **Signature** ```ts -export declare const fromCBORHexMap: (hex: string, options?: CBOR.CodecOptions) => Redeemers +export declare const fromCBORHexMap: (hex: string, options?: CBOR.CodecOptions) => RedeemerMap ``` Added in v2.0.0 @@ -244,9 +411,7 @@ Added in v2.0.0 ## ArrayCDDLSchema -CDDL schema for Redeemers in array format. - -`redeemers = [ + redeemer ]` +CDDL schema for array format: `[ + redeemer ]` **Signature** @@ -267,19 +432,16 @@ Added in v2.0.0 ## CDDLSchema -Default CDDL schema for Redeemers (array format). +Default CDDL schema (map format — Conway recommended). **Signature** ```ts -export declare const CDDLSchema: Schema.Array$< - Schema.Tuple< - [ - Schema.SchemaClass, - Schema.SchemaClass, - Schema.Schema, - Schema.Tuple2 - ] +export declare const CDDLSchema: Schema.MapFromSelf< + Schema.Tuple2, + Schema.Tuple2< + Schema.Schema, + Schema.Tuple2 > > ``` @@ -288,7 +450,7 @@ Added in v2.0.0 ## FromArrayCDDL -CDDL transformation schema for Redeemers array format. +CDDL transformation for array format → `RedeemerArray`. **Signature** @@ -304,7 +466,7 @@ export declare const FromArrayCDDL: Schema.transformOrFail< ] > >, - Schema.SchemaClass, + Schema.SchemaClass, never > ``` @@ -313,7 +475,7 @@ Added in v2.0.0 ## FromCBORBytes -CBOR bytes transformation schema for Redeemers (array format). +CBOR bytes schema for array format. **Signature** @@ -337,7 +499,7 @@ export declare const FromCBORBytes: ( ] > >, - Schema.SchemaClass, + Schema.SchemaClass, never > > @@ -347,7 +509,7 @@ Added in v2.0.0 ## FromCBORBytesMap -CBOR bytes transformation schema for Redeemers (map format). +CBOR bytes schema for map format. **Signature** @@ -361,14 +523,14 @@ export declare const FromCBORBytesMap: ( never >, Schema.transformOrFail< - Schema.Map$< + Schema.MapFromSelf< Schema.Tuple2, Schema.Tuple2< Schema.Schema, Schema.Tuple2 > >, - Schema.SchemaClass, + Schema.SchemaClass, never > > @@ -378,7 +540,7 @@ Added in v2.0.0 ## FromCBORHex -CBOR hex transformation schema for Redeemers (array format). +CBOR hex schema for array format. **Signature** @@ -404,7 +566,7 @@ export declare const FromCBORHex: ( ] > >, - Schema.SchemaClass, + Schema.SchemaClass, never > > @@ -415,7 +577,7 @@ Added in v2.0.0 ## FromCBORHexMap -CBOR hex transformation schema for Redeemers (map format). +CBOR hex schema for map format. **Signature** @@ -431,14 +593,14 @@ export declare const FromCBORHexMap: ( never >, Schema.transformOrFail< - Schema.Map$< + Schema.MapFromSelf< Schema.Tuple2, Schema.Tuple2< Schema.Schema, Schema.Tuple2 > >, - Schema.SchemaClass, + Schema.SchemaClass, never > > @@ -449,23 +611,20 @@ Added in v2.0.0 ## FromCDDL -Default CDDL transformation (array format). +Default CDDL transformation (map format). **Signature** ```ts export declare const FromCDDL: Schema.transformOrFail< - Schema.Array$< - Schema.Tuple< - [ - Schema.SchemaClass, - Schema.SchemaClass, - Schema.Schema, - Schema.Tuple2 - ] + Schema.MapFromSelf< + Schema.Tuple2, + Schema.Tuple2< + Schema.Schema, + Schema.Tuple2 > >, - Schema.SchemaClass, + Schema.SchemaClass, never > ``` @@ -474,20 +633,20 @@ Added in v2.0.0 ## FromMapCDDL -CDDL transformation schema for Redeemers map format. +CDDL transformation for map format → `RedeemerMap`. **Signature** ```ts export declare const FromMapCDDL: Schema.transformOrFail< - Schema.Map$< + Schema.MapFromSelf< Schema.Tuple2, Schema.Tuple2< Schema.Schema, Schema.Tuple2 > >, - Schema.SchemaClass, + Schema.SchemaClass, never > ``` @@ -496,14 +655,16 @@ Added in v2.0.0 ## MapCDDLSchema -CDDL schema for Redeemers in map format. +CDDL schema for map format: `{ + [tag, index] => [data, ex_units] }` -`{ + [tag, index] => [data, ex_units] }` +Uses `MapFromSelf` (not `Map`) so the Encoded type is a JS Map — matching +how `CBOR.FromBytes` represents CBOR major-type-5 maps at runtime. +This is the same pattern used by Withdrawals, Mint, MultiAsset, CostModel. **Signature** ```ts -export declare const MapCDDLSchema: Schema.Map$< +export declare const MapCDDLSchema: Schema.MapFromSelf< Schema.Tuple2, Schema.Tuple2< Schema.Schema, @@ -513,3 +674,30 @@ export declare const MapCDDLSchema: Schema.Map$< ``` Added in v2.0.0 + +## Redeemers + +Union schema for redeemers — accepts either map or array format. +Follows the Credential pattern: `Credential = Union(KeyHash, ScriptHash)`. + +**Signature** + +```ts +export declare const Redeemers: Schema.Union<[typeof RedeemerMap, typeof RedeemerArray]> +``` + +Added in v2.0.0 + +# utilities + +## keyToString + +Create a string key from a RedeemerKey for lookup convenience. + +**Signature** + +```ts +export declare const keyToString: ([tag, index]: RedeemerKey) => string +``` + +Added in v2.0.0 diff --git a/docs/content/docs/modules/TSchema.mdx b/docs/content/docs/modules/TSchema.mdx index 99a264bb..422bbacf 100644 --- a/docs/content/docs/modules/TSchema.mdx +++ b/docs/content/docs/modules/TSchema.mdx @@ -18,6 +18,7 @@ parent: Modules - [schemas](#schemas) - [ByteArray](#bytearray) - [Integer](#integer) + - [PlutusData](#plutusdata) - [utils](#utils) - [Array](#array) - [Array (interface)](#array-interface) @@ -32,6 +33,7 @@ parent: Modules - [Map (interface)](#map-interface) - [NullOr](#nullor) - [NullOr (interface)](#nullor-interface) + - [PlutusData (interface)](#plutusdata-interface) - [Struct](#struct) - [Struct (interface)](#struct-interface) - [StructOptions (interface)](#structoptions-interface) @@ -133,6 +135,21 @@ export declare const Integer: Integer Added in v2.0.0 +## PlutusData + +Opaque PlutusData schema for use inside TSchema combinators. +Represents an arbitrary PlutusData value that passes through encoding unchanged. + +Use this when a field can hold any PlutusData without a specific schema. + +**Signature** + +```ts +export declare const PlutusData: PlutusData +``` + +Added in v2.0.0 + # utils ## Array @@ -298,6 +315,14 @@ export interface NullOr extends Schema.transform, Schema.NullOr> {} ``` +## PlutusData (interface) + +**Signature** + +```ts +export interface PlutusData extends Schema.Schema {} +``` + ## Struct Creates a schema for struct types using Plutus Data Constructor diff --git a/docs/content/docs/modules/Transaction.mdx b/docs/content/docs/modules/Transaction.mdx index dee0f686..f595f3fd 100644 --- a/docs/content/docs/modules/Transaction.mdx +++ b/docs/content/docs/modules/Transaction.mdx @@ -10,6 +10,13 @@ parent: Modules

Table of contents

+- [encoding](#encoding) + - [addVKeyWitnesses](#addvkeywitnesses) + - [addVKeyWitnessesBytes](#addvkeywitnessesbytes) + - [addVKeyWitnessesHex](#addvkeywitnesseshex) + - [extractBodyBytes](#extractbodybytes) + - [toCBORBytesWithFormat](#tocborbyteswithformat) + - [toCBORHexWithFormat](#tocborhexwithformat) - [model](#model) - [Transaction (class)](#transaction-class) - [toJSON (method)](#tojson-method) @@ -17,6 +24,9 @@ parent: Modules - [[Inspectable.NodeInspectSymbol] (method)](#inspectablenodeinspectsymbol-method) - [[Equal.symbol] (method)](#equalsymbol-method) - [[Hash.symbol] (method)](#hashsymbol-method) +- [parsing](#parsing) + - [fromCBORBytesWithFormat](#fromcborbyteswithformat) + - [fromCBORHexWithFormat](#fromcborhexwithformat) - [utils](#utils) - [CDDLSchema](#cddlschema) - [FromCBORBytes](#fromcborbytes) @@ -30,6 +40,102 @@ parent: Modules --- +# encoding + +## addVKeyWitnesses + +Add VKey witnesses to a transaction at the domain level. + +This creates a new Transaction with the additional witnesses merged in. +All encoding metadata (body bytes, redeemers format, witness map structure) +is preserved so that txId and scriptDataHash remain stable. + +**Signature** + +```ts +export declare const addVKeyWitnesses: ( + tx: Transaction, + witnesses: ReadonlyArray +) => Transaction +``` + +Added in v2.0.0 + +## addVKeyWitnessesBytes + +Merge wallet vkey witnesses into a transaction at the raw CBOR byte level. + +Works like CML: the entire transaction byte stream is preserved except for +the vkey witnesses value in the witness set map. Body, redeemers, datums, +scripts, isValid, auxiliaryData, and map entry ordering stay byte-for-byte +identical — preserving both the txId and scriptDataHash. + +**Signature** + +```ts +export declare const addVKeyWitnessesBytes: ( + txBytes: Uint8Array, + walletWitnessSetBytes: Uint8Array, + options?: CBOR.CodecOptions +) => Uint8Array +``` + +Added in v2.0.0 + +## addVKeyWitnessesHex + +Hex variant of `addVKeyWitnessesBytes`. + +**Signature** + +```ts +export declare const addVKeyWitnessesHex: ( + txHex: string, + walletWitnessSetHex: string, + options?: CBOR.CodecOptions +) => string +``` + +Added in v2.0.0 + +## extractBodyBytes + +Extract the original body bytes from a raw transaction CBOR byte array. +A Cardano transaction is a 4-element CBOR array: `[body, witnessSet, isValid, auxiliaryData]`. +This returns the raw body bytes without decoding/re-encoding, preserving the exact CBOR encoding. + +**Signature** + +```ts +export declare const extractBodyBytes: (txBytes: Uint8Array) => Uint8Array +``` + +Added in v2.0.0 + +## toCBORBytesWithFormat + +Convert a Transaction to CBOR bytes using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORBytesWithFormat: (data: Transaction, format: CBOR.CBORFormat) => Uint8Array +``` + +Added in v2.0.0 + +## toCBORHexWithFormat + +Convert a Transaction to CBOR hex string using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORHexWithFormat: (data: Transaction, format: CBOR.CBORFormat) => string +``` + +Added in v2.0.0 + # model ## Transaction (class) @@ -87,6 +193,32 @@ toString(): string [Hash.symbol](): number ``` +# parsing + +## fromCBORBytesWithFormat + +Parse a Transaction from CBOR bytes and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORBytesWithFormat: (bytes: Uint8Array) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + +## fromCBORHexWithFormat + +Parse a Transaction from CBOR hex string and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORHexWithFormat: (hex: string) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + # utils ## CDDLSchema @@ -98,13 +230,11 @@ CDDL: transaction = [transaction_body, transaction_witness_set, bool, auxiliary_ **Signature** ```ts -export declare const CDDLSchema: Schema.Tuple< - [ - Schema.MapFromSelf>, - Schema.MapFromSelf>, - typeof Schema.Boolean, - Schema.Schema - ] +export declare const CDDLSchema: Schema.declare< + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [], + never > ``` @@ -124,13 +254,11 @@ export declare const FromCBORBytes: ( never >, Schema.transformOrFail< - Schema.Tuple< - [ - Schema.MapFromSelf>, - Schema.MapFromSelf>, - typeof Schema.Boolean, - Schema.Schema - ] + Schema.declare< + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [], + never >, Schema.SchemaClass, never @@ -157,13 +285,11 @@ export declare const FromCBORHex: ( > >, Schema.transformOrFail< - Schema.Tuple< - [ - Schema.MapFromSelf>, - Schema.MapFromSelf>, - typeof Schema.Boolean, - Schema.Schema - ] + Schema.declare< + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [], + never >, Schema.SchemaClass, never @@ -179,13 +305,11 @@ Transform between CDDL tuple and Transaction class. ```ts export declare const FromCDDL: Schema.transformOrFail< - Schema.Tuple< - [ - Schema.MapFromSelf>, - Schema.MapFromSelf>, - typeof Schema.Boolean, - Schema.Schema - ] + Schema.declare< + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [Map, Map, boolean, CBOR.CBOR], + readonly [], + never >, Schema.SchemaClass, never diff --git a/docs/content/docs/modules/TransactionBody.mdx b/docs/content/docs/modules/TransactionBody.mdx index 22b89d45..66b7ba0b 100644 --- a/docs/content/docs/modules/TransactionBody.mdx +++ b/docs/content/docs/modules/TransactionBody.mdx @@ -14,9 +14,13 @@ parent: Modules - [arbitrary](#arbitrary-1) - [conversion](#conversion) - [fromCBORBytes](#fromcborbytes) + - [fromCBORBytesWithFormat](#fromcborbyteswithformat) - [fromCBORHex](#fromcborhex) + - [fromCBORHexWithFormat](#fromcborhexwithformat) - [toCBORBytes](#tocborbytes) + - [toCBORBytesWithFormat](#tocborbyteswithformat) - [toCBORHex](#tocborhex) + - [toCBORHexWithFormat](#tocborhexwithformat) - [model](#model) - [TransactionBody (class)](#transactionbody-class) - [toJSON (method)](#tojson-method) @@ -66,6 +70,18 @@ export declare const fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOpti Added in v2.0.0 +## fromCBORBytesWithFormat + +Parse a TransactionBody from CBOR bytes and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORBytesWithFormat: (bytes: Uint8Array) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + ## fromCBORHex Convert CBOR hex string to TransactionBody. @@ -78,6 +94,18 @@ export declare const fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => Added in v2.0.0 +## fromCBORHexWithFormat + +Parse a TransactionBody from CBOR hex string and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORHexWithFormat: (hex: string) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + ## toCBORBytes Convert TransactionBody to CBOR bytes. @@ -90,6 +118,18 @@ export declare const toCBORBytes: (data: TransactionBody, options?: CBOR.CodecOp Added in v2.0.0 +## toCBORBytesWithFormat + +Convert a TransactionBody to CBOR bytes using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORBytesWithFormat: (data: TransactionBody, format: CBOR.CBORFormat) => Uint8Array +``` + +Added in v2.0.0 + ## toCBORHex Convert TransactionBody to CBOR hex string. @@ -102,6 +142,18 @@ export declare const toCBORHex: (data: TransactionBody, options?: CBOR.CodecOpti Added in v2.0.0 +## toCBORHexWithFormat + +Convert a TransactionBody to CBOR hex string using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORHexWithFormat: (data: TransactionBody, format: CBOR.CBORFormat) => string +``` + +Added in v2.0.0 + # model ## TransactionBody (class) @@ -195,10 +247,7 @@ CDDL schema for TransactionBody struct structure. **Signature** ```ts -export declare const CDDLSchema: Schema.MapFromSelf< - typeof Schema.BigIntFromSelf, - Schema.Schema -> +export declare const CDDLSchema: Schema.declare, Map, readonly [], never> ``` Added in v2.0.0 @@ -220,7 +269,7 @@ export declare const FromCBORBytes: ( never >, Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > @@ -249,7 +298,7 @@ export declare const FromCBORHex: ( > >, Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > @@ -266,7 +315,7 @@ Added in v2.0.0 ```ts export declare const FromCDDL: Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > diff --git a/docs/content/docs/modules/TransactionMetadatum.mdx b/docs/content/docs/modules/TransactionMetadatum.mdx index 673bea81..ea3acc42 100644 --- a/docs/content/docs/modules/TransactionMetadatum.mdx +++ b/docs/content/docs/modules/TransactionMetadatum.mdx @@ -20,6 +20,8 @@ parent: Modules - [encoding](#encoding) - [toCBORBytes](#tocborbytes) - [toCBORHex](#tocborhex) +- [equality](#equality) + - [equals](#equals) - [model](#model) - [List (type alias)](#list-type-alias) - [Map (type alias)](#map-type-alias) @@ -37,8 +39,6 @@ parent: Modules - [MapSchema](#mapschema) - [TextSchema](#textschema) - [TransactionMetadatumSchema](#transactionmetadatumschema) -- [utilities](#utilities) - - [equals](#equals) - [utils](#utils) - [arbitrary](#arbitrary) @@ -144,6 +144,22 @@ export declare const toCBORHex: (data: TransactionMetadatum, options?: CBOR.Code Added in v2.0.0 +# equality + +## equals + +Schema-derived structural equality for TransactionMetadatum values. +Handles maps, lists, ints, bytes, and text via the +recursive TransactionMetadatumSchema definition — no hand-rolled comparison needed. + +**Signature** + +```ts +export declare const equals: (a: TransactionMetadatum, b: TransactionMetadatum) => boolean +``` + +Added in v2.0.0 + # model ## List (type alias) @@ -436,20 +452,6 @@ export declare const TransactionMetadatumSchema: Schema.Union< Added in v2.0.0 -# utilities - -## equals - -Check if two TransactionMetadatum instances are equal. - -**Signature** - -```ts -export declare const equals: (a: TransactionMetadatum, b: TransactionMetadatum) => boolean -``` - -Added in v2.0.0 - # utils ## arbitrary diff --git a/docs/content/docs/modules/TransactionWitnessSet.mdx b/docs/content/docs/modules/TransactionWitnessSet.mdx index c310cf0f..6431d15c 100644 --- a/docs/content/docs/modules/TransactionWitnessSet.mdx +++ b/docs/content/docs/modules/TransactionWitnessSet.mdx @@ -18,7 +18,9 @@ parent: Modules - [fromVKeyWitnesses](#fromvkeywitnesses) - [encoding](#encoding) - [toCBORBytes](#tocborbytes) + - [toCBORBytesWithFormat](#tocborbyteswithformat) - [toCBORHex](#tocborhex) + - [toCBORHexWithFormat](#tocborhexwithformat) - [model](#model) - [PlutusScript](#plutusscript) - [TransactionWitnessSet (class)](#transactionwitnessset-class) @@ -35,7 +37,9 @@ parent: Modules - [[Hash.symbol] (method)](#hashsymbol-method-1) - [parsing](#parsing) - [fromCBORBytes](#fromcborbytes) + - [fromCBORBytesWithFormat](#fromcborbyteswithformat) - [fromCBORHex](#fromcborhex) + - [fromCBORHexWithFormat](#fromcborhexwithformat) - [schemas](#schemas) - [CDDLSchema](#cddlschema) - [FromCDDL](#fromcddl) @@ -112,6 +116,18 @@ export declare const toCBORBytes: (data: TransactionWitnessSet, options?: CBOR.C Added in v2.0.0 +## toCBORBytesWithFormat + +Convert a TransactionWitnessSet to CBOR bytes using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORBytesWithFormat: (data: TransactionWitnessSet, format: CBOR.CBORFormat) => Uint8Array +``` + +Added in v2.0.0 + ## toCBORHex Convert a TransactionWitnessSet to CBOR hex string. @@ -124,6 +140,18 @@ export declare const toCBORHex: (data: TransactionWitnessSet, options?: CBOR.Cod Added in v2.0.0 +## toCBORHexWithFormat + +Convert a TransactionWitnessSet to CBOR hex string using an explicit root format tree. + +**Signature** + +```ts +export declare const toCBORHexWithFormat: (data: TransactionWitnessSet, format: CBOR.CBORFormat) => string +``` + +Added in v2.0.0 + # model ## PlutusScript @@ -302,6 +330,18 @@ export declare const fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOpti Added in v2.0.0 +## fromCBORBytesWithFormat + +Parse a TransactionWitnessSet from CBOR bytes and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORBytesWithFormat: (bytes: Uint8Array) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + ## fromCBORHex Parse a TransactionWitnessSet from CBOR hex string. @@ -314,6 +354,18 @@ export declare const fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => Added in v2.0.0 +## fromCBORHexWithFormat + +Parse a TransactionWitnessSet from CBOR hex string and return the root format tree. + +**Signature** + +```ts +export declare const fromCBORHexWithFormat: (hex: string) => CBOR.DecodedWithFormat +``` + +Added in v2.0.0 + # schemas ## CDDLSchema @@ -337,10 +389,7 @@ nonempty_set = #6.258([+ a0]) / [+ a0] **Signature** ```ts -export declare const CDDLSchema: Schema.MapFromSelf< - typeof Schema.BigIntFromSelf, - Schema.Schema -> +export declare const CDDLSchema: Schema.declare, Map, readonly [], never> ``` Added in v2.0.0 @@ -353,7 +402,7 @@ CDDL transformation schema for TransactionWitnessSet. ```ts export declare const FromCDDL: Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > @@ -377,7 +426,7 @@ export declare const FromCBORBytes: ( never >, Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > @@ -401,7 +450,7 @@ export declare const FromCBORHex: ( > >, Schema.transformOrFail< - Schema.MapFromSelf>, + Schema.declare, Map, readonly [], never>, Schema.SchemaClass, never > diff --git a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx index e3b210e5..6f5a0e63 100644 --- a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx +++ b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx @@ -984,6 +984,9 @@ export interface ProtocolParameters { /** Price per CPU step for script execution (optional, for ExUnits cost calculation) */ priceStep?: number + /** Cost per byte for reference scripts (Conway-era, default 44) */ + minFeeRefScriptCostPerByte?: number + // Future fields for advanced features: // maxBlockHeaderSize?: number // maxTxExecutionUnits?: ExUnits @@ -1735,12 +1738,9 @@ export interface BuildOptions { /** * Format for encoding redeemers in the script data hash. * - * - `"array"` (DEFAULT): Conway-era format, redeemers encoded as array - * - `"map"`: Babbage-era format, redeemers encoded as map - * - * Use `"map"` for Babbage compatibility or debugging. + * @deprecated Redeemer format is now determined by the concrete `Redeemers` type + * (`RedeemerMap` or `RedeemerArray`). This option is ignored. * - * @default "array" * @since 2.0.0 */ readonly scriptDataFormat?: "array" | "map" diff --git a/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx b/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx index cb3f8889..3f1063e8 100644 --- a/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx +++ b/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx @@ -33,6 +33,7 @@ parent: Modules - [makeTxOutput](#maketxoutput) - [mergeAssetsIntoOutput](#mergeassetsintooutput) - [mergeAssetsIntoUTxO](#mergeassetsintoutxo) + - [tierRefScriptFee](#tierrefscriptfee) - [validation](#validation) - [calculateLeftoverAssets](#calculateleftoverassets) - [validateTransactionBalance](#validatetransactionbalance) @@ -87,7 +88,11 @@ Added in v2.0.0 ## calculateMinimumUtxoLovelace Calculate minimum ADA required for a UTxO based on its actual CBOR size. -Uses the Babbage-era formula: coinsPerUtxoByte \* utxoSize. +Uses the Babbage/Conway-era formula: coinsPerUtxoByte \* (160 + serializedOutputSize). + +The 160-byte constant accounts for the UTxO entry overhead in the ledger state +(transaction hash + index). A lovelace placeholder is used during CBOR encoding +to ensure the coin field width matches the final result. This function creates a temporary TransactionOutput, encodes it to CBOR, and calculates the exact size to determine the minimum lovelace required. @@ -248,18 +253,24 @@ Added in v2.0.0 Calculate reference script fees using tiered pricing. -Reference scripts stored on-chain incur additional fees based on their size: +Matches the Cardano node's `tierRefScriptFee` from Conway ledger: + +- Stride: 25,600 bytes (hardcoded, becomes a protocol param post-Conway) +- Multiplier: 1.2× per tier (hardcoded, becomes a protocol param post-Conway) +- Base cost: `minFeeRefScriptCostPerByte` protocol parameter -- First 25KB: 15 lovelace/byte -- Next 25KB: 25 lovelace/byte -- Next 150KB: 100 lovelace/byte -- Maximum: 200KB total +For each 25,600-byte chunk the price per byte increases by 1.2×. +The final (partial) chunk is charged proportionally. Result is `floor(total)`. + +The Cardano node sums scriptRef sizes from both spent inputs and reference +inputs (`txNonDistinctRefScriptsSize`), so callers must pass both. **Signature** ```ts export declare const calculateReferenceScriptFee: ( - referenceInputs: ReadonlyArray + utxos: ReadonlyArray, + costPerByte: number ) => Effect.Effect ``` @@ -375,6 +386,27 @@ export declare const mergeAssetsIntoUTxO: ( Added in v2.0.0 +## tierRefScriptFee + +Calculate reference script fees using tiered pricing. + +Direct port of the Cardano ledger's `tierRefScriptFee` function. +Each `sizeIncrement`-byte chunk is priced at `curTierPrice` per byte, +then `curTierPrice *= multiplier` for the next chunk. Final result: `floor(total)`. + +**Signature** + +```ts +export declare const tierRefScriptFee: ( + multiplier: number, + sizeIncrement: number, + baseFee: number, + totalSize: number +) => bigint +``` + +Added in v2.0.0 + # validation ## calculateLeftoverAssets diff --git a/docs/content/docs/modules/utils/Hash.mdx b/docs/content/docs/modules/utils/Hash.mdx index b49b75a0..8c6a6c88 100644 --- a/docs/content/docs/modules/utils/Hash.mdx +++ b/docs/content/docs/modules/utils/Hash.mdx @@ -11,29 +11,16 @@ parent: Modules

Table of contents

- [utils](#utils) - - [RedeemersFormat (type alias)](#redeemersformat-type-alias) - [computeTotalExUnits](#computetotalexunits) - [hashAuxiliaryData](#hashauxiliarydata) - [hashScriptData](#hashscriptdata) - [hashTransaction](#hashtransaction) + - [hashTransactionRaw](#hashtransactionraw) --- # utils -## RedeemersFormat (type alias) - -Format for encoding redeemers in the script data hash. - -- "array": Legacy format `[ + redeemer ]` (Shelley-Babbage) -- "map": Conway format `{ + [tag, index] => [data, ex_units] }` - -**Signature** - -```ts -export type RedeemersFormat = "array" | "map" -``` - ## computeTotalExUnits Compute total ex_units by summing over redeemers. @@ -58,6 +45,9 @@ export declare const hashAuxiliaryData: (aux: AuxiliaryData.AuxiliaryData) => Au Compute script_data_hash using standard module encoders. +Accepts the concrete `Redeemers` union type — encoding format is determined +by `_tag` (`RedeemerMap` → map CBOR, `RedeemerArray` → array CBOR). + The payload format per CDDL spec is raw concatenation (not a CBOR structure): ``` @@ -68,10 +58,9 @@ redeemers_bytes || datums_bytes || language_views_bytes ```ts export declare const hashScriptData: ( - redeemers: ReadonlyArray, + redeemers: Redeemers.Redeemers, costModels: CostModel.CostModels, datums?: ReadonlyArray, - format?: RedeemersFormat, options?: CBOR.CodecOptions ) => ScriptDataHash.ScriptDataHash ``` @@ -85,3 +74,14 @@ Compute the transaction body hash (blake2b-256 over CBOR of body). ```ts export declare const hashTransaction: (body: TransactionBody.TransactionBody) => TransactionHash.TransactionHash ``` + +## hashTransactionRaw + +Compute the transaction body hash from raw CBOR bytes, preserving original encoding. +Uses `Transaction.extractBodyBytes` to avoid the decode→re-encode round-trip. + +**Signature** + +```ts +export declare const hashTransactionRaw: (bodyBytes: Uint8Array) => TransactionHash.TransactionHash +``` diff --git a/docs/content/docs/providers/index.mdx b/docs/content/docs/providers/index.mdx index 8242fa1d..94abb5c0 100644 --- a/docs/content/docs/providers/index.mdx +++ b/docs/content/docs/providers/index.mdx @@ -18,28 +18,25 @@ The SDK abstracts provider differences through a unified interface. Choose your Create a provider-only client to query blockchain data: ```typescript -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Query protocol parameters -const params = await client.getProtocolParameters(); +const params = await sdkClient.getProtocolParameters(); console.log("Min fee:", params.minFeeConstant); // Query any address -const utxos = await client.getUtxos("addr1..."); +const utxos = await sdkClient.getUtxos("addr1..."); console.log("UTxOs found:", utxos.length); // Submit pre-signed transaction const signedTxCbor = "84a300..."; // Signed transaction CBOR -const txHash = await client.submitTx(signedTxCbor); +const txHash = await sdkClient.submitTx(signedTxCbor); ``` ## Available Providers diff --git a/docs/content/docs/providers/provider-only-client.mdx b/docs/content/docs/providers/provider-only-client.mdx index 7459c763..ae1470b0 100644 --- a/docs/content/docs/providers/provider-only-client.mdx +++ b/docs/content/docs/providers/provider-only-client.mdx @@ -29,25 +29,22 @@ Provider-only clients are ideal when you need blockchain data but not wallet-spe Create a client with only provider configuration: ```typescript twoslash -import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, client, mainnet, blockfrost, kupmios, readOnlyWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Query any address -const utxos = await client.getUtxos( +const utxos = await sdkClient.getUtxos( Address.fromBech32("addr1qxy8sclc58rsck0pzsc0v4skmqjwuqsqpwfcvrdldl5sjvvhyltp7fk0fmtmrlnykgmhnzcns2msa2cmpvllzgqd2azqhpv8e4") ); console.log("Found UTxOs:", utxos.length); // Get protocol parameters -const params = await client.getProtocolParameters(); +const params = await sdkClient.getProtocolParameters(); console.log("Min fee A:", params.minFeeA); ``` @@ -56,25 +53,22 @@ console.log("Min fee A:", params.minFeeA); Provider-only clients expose all provider methods directly: ```typescript twoslash -import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, client, mainnet, blockfrost, kupmios, readOnlyWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Protocol parameters -const params = await client.getProtocolParameters(); +const params = await sdkClient.getProtocolParameters(); // UTxO queries - use Address.fromBech32 for addresses const address = Address.fromBech32("addr1qxy8sclc58rsck0pzsc0v4skmqjwuqsqpwfcvrdldl5sjvvhyltp7fk0fmtmrlnykgmhnzcns2msa2cmpvllzgqd2azqhpv8e4"); -const utxos = await client.getUtxos(address); -const utxosWithAda = await client.getUtxosWithUnit(address, "lovelace"); -const utxosByRefs = await client.getUtxosByOutRef([ +const utxos = await sdkClient.getUtxos(address); +const utxosWithAda = await sdkClient.getUtxosWithUnit(address, "lovelace"); +const utxosByRefs = await sdkClient.getUtxosByOutRef([ new TransactionInput.TransactionInput({ transactionId: TransactionHash.fromHex("abc123def456..."), index: 0n @@ -82,20 +76,20 @@ const utxosByRefs = await client.getUtxosByOutRef([ ]); // Delegation -const delegation = await client.getDelegation(RewardAddress.RewardAddress.make("stake1uxy...")); +const delegation = await sdkClient.getDelegation(RewardAddress.RewardAddress.make("stake1uxy...")); // Datum resolution -const datum = await client.getDatum(new DatumHash.DatumHash({ hash: Bytes.fromHex("datum-hash-here") })); +const datum = await sdkClient.getDatum(new DatumHash.DatumHash({ hash: Bytes.fromHex("datum-hash-here") })); // Transaction operations const signedTxCbor = "84a300..."; const signedTx = Transaction.fromCBORHex(signedTxCbor); -const txHash = await client.submitTx(signedTx); -const confirmed = await client.awaitTx(txHash); +const txHash = await sdkClient.submitTx(signedTx); +const confirmed = await sdkClient.awaitTx(txHash); const unsignedTxCbor = "84a300..."; const unsignedTx = Transaction.fromCBORHex(unsignedTxCbor); -const evaluated = await client.evaluateTx(unsignedTx); +const evaluated = await sdkClient.evaluateTx(unsignedTx); ``` ## Querying Multiple Addresses @@ -103,22 +97,19 @@ const evaluated = await client.evaluateTx(unsignedTx); Portfolio tracker for multiple addresses: ```typescript twoslash -import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, client, mainnet, blockfrost, kupmios, readOnlyWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Portfolio tracker async function getPortfolioBalance(addressesBech32: string[]) { const results = await Promise.all( addressesBech32.map(async (addressBech32) => { - const utxos = await client.getUtxos( + const utxos = await sdkClient.getUtxos( Address.fromBech32(addressBech32) ); const lovelace = utxos.reduce( @@ -143,25 +134,22 @@ const portfolio = await getPortfolioBalance([ Accept signed transactions and submit to blockchain: ```typescript twoslash -import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, client, mainnet, blockfrost, kupmios, readOnlyWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // API endpoint export async function submitTransaction(signedTxCbor: string) { try { const signedTx = Transaction.fromCBORHex(signedTxCbor); - const txHash = await client.submitTx(signedTx); + const txHash = await sdkClient.submitTx(signedTx); // Wait for confirmation - const confirmed = await client.awaitTx(txHash, 5000); + const confirmed = await sdkClient.awaitTx(txHash, 5000); return { success: true, @@ -182,19 +170,16 @@ export async function submitTransaction(signedTxCbor: string) { Track network parameters for fee estimation or protocol changes: ```typescript twoslash -import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, client, mainnet, blockfrost, kupmios, readOnlyWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function monitorProtocolParameters() { - const params = await client.getProtocolParameters(); + const params = await sdkClient.getProtocolParameters(); return { minFeeA: params.minFeeA, @@ -221,61 +206,46 @@ Provider-only clients cannot perform wallet-specific operations: ```typescript twoslash // @errors: 2339 -import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, client, mainnet, blockfrost, kupmios, readOnlyWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); - -// Cannot build transactions - no wallet address -// @ts-expect-error - Property 'newTx' does not exist on provider-only client -client.newTx(); +})); // Cannot sign - no private key -// @ts-expect-error - Property 'signTx' does not exist on provider-only client -client.signTx(); +sdkClient.signTx(); // Cannot get own address - no wallet -// @ts-expect-error - Property 'address' does not exist on provider-only client -client.address(); +sdkClient.getAddress(); // CAN query any address const address = Address.fromBech32("addr1qxy8sclc58rsck0pzsc0v4skmqjwuqsqpwfcvrdldl5sjvvhyltp7fk0fmtmrlnykgmhnzcns2msa2cmpvllzgqd2azqhpv8e4"); -const utxos = await client.getUtxos(address); +const utxos = await sdkClient.getUtxos(address); // CAN submit pre-signed transactions const signedCbor = "84a400..."; // Example signed transaction CBOR const signedTx = Transaction.fromCBORHex(signedCbor); -const txHash = await client.submitTx(signedTx); +const txHash = await sdkClient.submitTx(signedTx); ``` ## Upgrading to Full Client -Add wallet to provider-only client using `attachWallet()`: +Add wallet to provider-only client using `.with(wallet)`: ```typescript twoslash -import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, client, mainnet, blockfrost, kupmios, readOnlyWallet } from "@evolution-sdk/evolution"; // Start with provider only -const providerClient = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const providerClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Later, attach wallet for specific user -const readOnlyClient = providerClient.attachWallet({ - type: "read-only", - address: "addr1qxy8g0m3dnvxpk6dlh40u9vgc8m6hyqyjf6qn6j6t47wnhvcpqp0aw50nln8nyzfh6fjp6sxgajx5q0c6p73xqf2qhvq5pzqsh" -}); +const readOnlyClient = providerClient.with(readOnlyWallet("addr1qxy8g0m3dnvxpk6dlh40u9vgc8m6hyqyjf6qn6j6t47wnhvcpqp0aw50nln8nyzfh6fjp6sxgajx5q0c6p73xqf2qhvq5pzqsh")); // Now can build transactions for this address const builder = readOnlyClient.newTx(); @@ -292,13 +262,13 @@ const result = await builder.build(); Switch between environments: ```typescript twoslash -import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, Bytes, DatumHash, RewardAddress, Transaction, TransactionHash, TransactionInput, client, preprod, mainnet, blockfrost, kupmios, readOnlyWallet } from "@evolution-sdk/evolution"; const env = (process.env.NODE_ENV || "development") as "development" | "production"; const config = { development: { - network: "preprod" as const, + chain: preprod, provider: { type: "blockfrost" as const, baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", @@ -306,7 +276,7 @@ const config = { } }, production: { - network: "mainnet" as const, + chain: mainnet, provider: { type: "kupmios" as const, ogmiosUrl: process.env.OGMIOS_URL!, @@ -315,7 +285,12 @@ const config = { } }; -const client = createClient(config[env]); +const sdkClient = client(config[env].chain) + .with( + config[env].provider.type === "blockfrost" + ? blockfrost({ baseUrl: config[env].provider.baseUrl, projectId: config[env].provider.projectId! }) + : kupmios({ ogmiosUrl: config[env].provider.ogmiosUrl!, kupoUrl: config[env].provider.kupoUrl! }) + ); ``` ## Comparison with Read-Only Client @@ -323,9 +298,9 @@ const client = createClient(config[env]); | Feature | Provider-Only Client | Read-Only Client | |---------|---------------------|------------------| | **Configuration** | Provider only | Provider + address | -| **Creation** | `createClient({ provider })` | `createClient({ provider, wallet: { type: "read-only", address } })` | +| **Creation** | `client(chain).with(provider)` | `client(chain).with(provider).with(readOnlyWallet(address))` | | **Query any address** | `getUtxos(anyAddress)` | `getUtxos(anyAddress)` | -| **Query own address** | Not available | `getWalletUtxos()` | +| **Query own address** | Not available | `getUtxos()` (wallet UTxOs) | | **Build transactions** | Not available | `newTx()` returns unsigned tx | | **Sign transactions** | Not available | Not available | | **Submit transactions** | `submitTx(signedCbor)` | `submitTx(signedCbor)` | diff --git a/docs/content/docs/providers/provider-types.mdx b/docs/content/docs/providers/provider-types.mdx index 3300f9b5..5780794e 100644 --- a/docs/content/docs/providers/provider-types.mdx +++ b/docs/content/docs/providers/provider-types.mdx @@ -14,16 +14,13 @@ Hosted API service with generous free tier and pay-as-you-grow pricing. ### Configuration ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); ``` ### Network Endpoints @@ -76,16 +73,13 @@ Self-hosted combination of Ogmios (Cardano node interface) and Kupo (lightweight ### Configuration ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "kupmios", - ogmiosUrl: "http://localhost:1337", +const sdkClient = client(mainnet) + .with(kupmios({ +ogmiosUrl: "http://localhost:1337", kupoUrl: "http://localhost:1442" - } -}); +})); ``` ### Setup Requirements @@ -118,12 +112,12 @@ services: ```typescript // Mainnet -network: "mainnet", +chain: mainnet, ogmiosUrl: "http://localhost:1337", kupoUrl: "http://localhost:1442" // Preprod testnet -network: "preprod", +chain: preprod, ogmiosUrl: "http://localhost:1337", kupoUrl: "http://localhost:1442" ``` @@ -154,16 +148,13 @@ Hosted API service with advanced features and analytics capabilities. ### Configuration ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "maestro", - baseUrl: "https://mainnet.gomaestro-api.org/v1", +const sdkClient = client(mainnet) + .with(maestro({ +baseUrl: "https://mainnet.gomaestro-api.org/v1", apiKey: process.env.MAESTRO_API_KEY! - } -}); +})); ``` ### Network Endpoints @@ -215,15 +206,12 @@ Community-driven distributed API infrastructure. ### Configuration ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; - -const client = createClient({ - network: "mainnet", - provider: { - type: "koios", - baseUrl: "https://api.koios.rest/api/v1" - } -}); +import { client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; + +const sdkClient = client(mainnet) + .with(koios({ +baseUrl: "https://api.koios.rest/api/v1" +})); ``` ### Network Endpoints @@ -279,27 +267,21 @@ Applications prioritizing decentralization, community infrastructure, or wanting The unified interface allows switching providers with minimal code changes: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, preprod, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; // Development: Blockfrost -const devClient = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const devClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Production: Self-hosted Kupmios -const prodClient = createClient({ - network: "mainnet", - provider: { - type: "kupmios", - ogmiosUrl: process.env.OGMIOS_URL!, +const prodClient = client(mainnet) + .with(kupmios({ +ogmiosUrl: process.env.OGMIOS_URL!, kupoUrl: process.env.KUPO_URL! - } -}); +})); // Same query methods work across all providers const params = await devClient.getProtocolParameters(); @@ -310,32 +292,25 @@ const params = await devClient.getProtocolParameters(); Manage provider configuration per environment: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, preprod, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; const environment = (process.env.NODE_ENV || "development") as "development" | "staging" | "production"; -const providerConfig = { - development: { - type: "blockfrost" as const, - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", - projectId: process.env.BLOCKFROST_PREPROD_PROJECT_ID! - }, - staging: { - type: "maestro" as const, - baseUrl: "https://preprod.gomaestro-api.org/v1", - apiKey: process.env.MAESTRO_STAGING_API_KEY! - }, - production: { - type: "kupmios" as const, - ogmiosUrl: process.env.OGMIOS_URL!, - kupoUrl: process.env.KUPO_URL! - } -}; - -const client = createClient({ - network: environment === "production" ? "mainnet" : "preprod", - provider: providerConfig[environment] -}); +const sdkClient = + environment === "production" + ? client(mainnet).with(kupmios({ + ogmiosUrl: process.env.OGMIOS_URL!, + kupoUrl: process.env.KUPO_URL! + })) + : environment === "staging" + ? client(preprod).with(maestro({ + baseUrl: "https://preprod.gomaestro-api.org/v1", + apiKey: process.env.MAESTRO_STAGING_API_KEY! + })) + : client(preprod).with(blockfrost({ + baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", + projectId: process.env.BLOCKFROST_PREPROD_PROJECT_ID! + })); ``` ## Next Steps diff --git a/docs/content/docs/providers/querying.mdx b/docs/content/docs/providers/querying.mdx index 8653e27a..e3b60281 100644 --- a/docs/content/docs/providers/querying.mdx +++ b/docs/content/docs/providers/querying.mdx @@ -12,18 +12,15 @@ Providers expose methods to query UTxOs, protocol parameters, delegation informa Retrieve current network parameters for fee calculation and transaction constraints: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); -const params = await client.getProtocolParameters(); +const params = await sdkClient.getProtocolParameters(); // Fee calculation parameters console.log("Min fee A:", params.minFeeA); @@ -48,19 +45,16 @@ Query unspent transaction outputs by address, credential, unit, or reference. ### By Address ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Get all UTxOs at address -const utxos = await client.getUtxos( +const utxos = await sdkClient.getUtxos( Address.fromBech32("addr1qxy8sclc58rsck0pzsc0v4skmqjwuqsqpwfcvrdldl5sjvvhyltp7fk0fmtmrlnykgmhnzcns2msa2cmpvllzgqd2azqhpv8e4") ); @@ -86,22 +80,19 @@ utxos.forEach((utxo) => { Query UTxOs by payment credential instead of full address: ```typescript -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Create credential from key hash const credential = Credential.keyHash("payment-key-hash-here"); // Query by credential -const utxos = await client.getUtxos(credential); +const utxos = await sdkClient.getUtxos(credential); ``` ### By Unit @@ -109,23 +100,20 @@ const utxos = await client.getUtxos(credential); Query UTxOs containing specific native asset: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Find UTxOs with specific token at address const policyId = "abc123def456abc123def456abc123def456abc123def456abc123de7890"; const assetName = "MyToken"; const unit = policyId + assetName; -const utxosWithToken = await client.getUtxosWithUnit( +const utxosWithToken = await sdkClient.getUtxosWithUnit( Address.fromBech32("addr1qxy8sclc58rsck0pzsc0v4skmqjwuqsqpwfcvrdldl5sjvvhyltp7fk0fmtmrlnykgmhnzcns2msa2cmpvllzgqd2azqhpv8e4"), unit ); @@ -138,19 +126,16 @@ console.log("UTxOs with token:", utxosWithToken.length); Query specific UTxOs by transaction output reference: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Query specific UTxOs -const utxos = await client.getUtxosByOutRef([ +const utxos = await sdkClient.getUtxosByOutRef([ new TransactionInput.TransactionInput({ transactionId: TransactionHash.fromHex("abc123..."), index: 0n @@ -169,20 +154,17 @@ console.log("Found UTxOs:", utxos.length); Find a single UTxO by unique unit identifier: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Query NFT (unique token with quantity 1) const nftUnit = "policyId" + "tokenName"; -const utxo = await client.getUtxoByUnit(nftUnit); +const utxo = await sdkClient.getUtxoByUnit(nftUnit); console.log("NFT found at:", Address.toBech32(utxo.address)); console.log("Current owner UTxO:", TransactionHash.toHex(utxo.transactionId)); @@ -193,18 +175,15 @@ console.log("Current owner UTxO:", TransactionHash.toHex(utxo.transactionId)); Query staking delegation and reward information: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); -const delegation = await client.getDelegation(RewardAddress.RewardAddress.make("stake1uxy...")); +const delegation = await sdkClient.getDelegation(RewardAddress.RewardAddress.make("stake1uxy...")); console.log("Delegated to pool:", delegation.poolId); console.log("Is delegated:", delegation.poolId !== undefined); @@ -216,20 +195,17 @@ console.log("Rewards:", delegation.rewards); Retrieve datum content by hash: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Get datum from hash const datumHash = new DatumHash.DatumHash({ hash: Bytes.fromHex("abc123...") }); -const datumCbor = await client.getDatum(datumHash); +const datumCbor = await sdkClient.getDatum(datumHash); console.log("Datum CBOR:", datumCbor); ``` @@ -241,22 +217,19 @@ console.log("Datum CBOR:", datumCbor); Calculate total balance across multiple addresses: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function getPortfolioBalance(addressesBech32: string[]) { const balances = await Promise.all( addressesBech32.map(async (addressBech32) => { const address = Address.fromBech32(addressBech32); - const utxos = await client.getUtxos(address); + const utxos = await sdkClient.getUtxos(address); const lovelace = utxos.reduce( (sum, utxo) => sum + utxo.assets.lovelace, 0n @@ -290,16 +263,13 @@ async function getPortfolioBalance(addressesBech32: string[]) { Find all holders of a specific token: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function getTokenHolders( addresses: string[], @@ -309,7 +279,7 @@ async function getTokenHolders( for (const addressBech32 of addresses) { const address = Address.fromBech32(addressBech32); - const utxos = await client.getUtxosWithUnit(address, tokenUnit); + const utxos = await sdkClient.getUtxosWithUnit(address, tokenUnit); if (utxos.length > 0) { // Count UTxOs containing this token @@ -330,16 +300,13 @@ async function getTokenHolders( Check if addresses are delegated to specific pool: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function checkPoolDelegation( rewardAddresses: string[], @@ -347,7 +314,7 @@ async function checkPoolDelegation( ) { const results = await Promise.all( rewardAddresses.map(async (rewardAddress) => { - const delegation = await client.getDelegation(RewardAddress.RewardAddress.make(rewardAddress)); + const delegation = await sdkClient.getDelegation(RewardAddress.RewardAddress.make(rewardAddress)); return { rewardAddress, @@ -368,22 +335,19 @@ async function checkPoolDelegation( Track NFT ownership across collection: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function getNFTOwnership(nftUnits: string[]) { const ownership = await Promise.all( nftUnits.map(async (unit) => { try { - const utxo = await client.getUtxoByUnit(unit); + const utxo = await sdkClient.getUtxoByUnit(unit); return { unit, owner: Address.toBech32(utxo.address), @@ -409,21 +373,18 @@ async function getNFTOwnership(nftUnits: string[]) { Handle query errors gracefully: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function safeGetUtxos(addressBech32: string) { try { const address = Address.fromBech32(addressBech32); - const utxos = await client.getUtxos(address); + const utxos = await sdkClient.getUtxos(address); return { success: true as const, utxos }; } catch (error: any) { console.error("Failed to query UTxOs:", error); @@ -433,7 +394,7 @@ async function safeGetUtxos(addressBech32: string) { async function safeGetDelegation(rewardAddress: string) { try { - const delegation = await client.getDelegation(RewardAddress.RewardAddress.make(rewardAddress)); + const delegation = await sdkClient.getDelegation(RewardAddress.RewardAddress.make(rewardAddress)); return { success: true as const, delegation }; } catch (error: any) { // May fail if address not registered @@ -447,23 +408,20 @@ async function safeGetDelegation(rewardAddress: string) { Optimize queries for better performance: ```typescript twoslash -import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, createClient } from "@evolution-sdk/evolution"; +import { Address, Bytes, Credential, DatumHash, PoolKeyHash, RewardAddress, TransactionHash, TransactionInput, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Batch queries in parallel async function batchQuery(addressesBech32: string[]) { // Good: parallel queries const results = await Promise.all( addressesBech32.map(addr => - client.getUtxos(Address.fromBech32(addr)) + sdkClient.getUtxos(Address.fromBech32(addr)) ) ); @@ -471,7 +429,7 @@ async function batchQuery(addressesBech32: string[]) { } // Cache protocol parameters -type ProtocolParams = Awaited>; +type ProtocolParams = Awaited>; let cachedParams: ProtocolParams | null = null; let cacheTime = 0; const CACHE_DURATION = 300000; // 5 minutes @@ -480,7 +438,7 @@ async function getCachedProtocolParameters() { const now = Date.now(); if (!cachedParams || now - cacheTime > CACHE_DURATION) { - cachedParams = await client.getProtocolParameters(); + cachedParams = await sdkClient.getProtocolParameters(); cacheTime = now; } diff --git a/docs/content/docs/providers/submission.mdx b/docs/content/docs/providers/submission.mdx index 5e54aa58..97079ec2 100644 --- a/docs/content/docs/providers/submission.mdx +++ b/docs/content/docs/providers/submission.mdx @@ -12,21 +12,18 @@ Providers handle transaction submission and confirmation monitoring. Submit pre- Submit a signed transaction CBOR string: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); // Submit signed transaction const signedTxCbor = "84a300..."; // Signed transaction CBOR const signedTx = Transaction.fromCBORHex(signedTxCbor); -const txHash = await client.submitTx(signedTx); +const txHash = await sdkClient.submitTx(signedTx); console.log("Transaction submitted:", txHash); ``` @@ -36,23 +33,20 @@ console.log("Transaction submitted:", txHash); Monitor transaction until confirmed on blockchain: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); const signedTxCbor = "84a300..."; const signedTx = Transaction.fromCBORHex(signedTxCbor); -const txHash = await client.submitTx(signedTx); +const txHash = await sdkClient.submitTx(signedTx); // Wait for confirmation (checks every 5 seconds by default) -const confirmed = await client.awaitTx(txHash); +const confirmed = await sdkClient.awaitTx(txHash); if (confirmed) { console.log("Transaction confirmed!"); @@ -66,22 +60,19 @@ if (confirmed) { Specify how often to check for confirmation: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); const txHashHex = "abc123..."; const txHash = TransactionHash.fromHex(txHashHex); // Check every 10 seconds -const confirmed = await client.awaitTx(txHash, 10000); +const confirmed = await sdkClient.awaitTx(txHash, 10000); console.log("Confirmed:", confirmed); ``` @@ -91,22 +82,19 @@ console.log("Confirmed:", confirmed); Evaluate transaction before submission to estimate script execution costs: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); const unsignedTxCbor = "84a300..."; // Unsigned transaction const unsignedTx = Transaction.fromCBORHex(unsignedTxCbor); // Evaluate script execution -const redeemers = await client.evaluateTx(unsignedTx); +const redeemers = await sdkClient.evaluateTx(unsignedTx); redeemers.forEach((redeemer) => { console.log("Redeemer tag:", redeemer.redeemer_tag); @@ -121,16 +109,13 @@ redeemers.forEach((redeemer) => { Create a transaction submission service with error handling: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface SubmissionResult { success: boolean; @@ -145,11 +130,11 @@ export async function submitAndWait( ): Promise { try { const signedTx = Transaction.fromCBORHex(signedTxCbor); - const txHash = await client.submitTx(signedTx); + const txHash = await sdkClient.submitTx(signedTx); console.log("Transaction submitted:", txHash); - const confirmed = await client.awaitTx(txHash, checkInterval); + const confirmed = await sdkClient.awaitTx(txHash, checkInterval); return { success: true, @@ -172,16 +157,13 @@ export async function submitAndWait( Submit multiple transactions sequentially: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function submitBatch(signedTxs: string[]) { const results = []; @@ -189,10 +171,10 @@ async function submitBatch(signedTxs: string[]) { for (const txCbor of signedTxs) { try { const tx = Transaction.fromCBORHex(txCbor); - const txHash = await client.submitTx(tx); + const txHash = await sdkClient.submitTx(tx); console.log("Submitted:", txHash); - const confirmed = await client.awaitTx(txHash); + const confirmed = await sdkClient.awaitTx(txHash); results.push({ success: true, @@ -216,21 +198,18 @@ async function submitBatch(signedTxs: string[]) { Handle common submission errors: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function safeSubmit(signedTxCbor: string) { try { const signedTx = Transaction.fromCBORHex(signedTxCbor); - const txHash = await client.submitTx(signedTx); + const txHash = await sdkClient.submitTx(signedTx); return { success: true as const, txHash }; } catch (error: any) { // Common errors @@ -275,16 +254,13 @@ async function safeSubmit(signedTxCbor: string) { Implement retry logic for transient failures: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function submitWithRetry( signedTxCbor: string, @@ -296,7 +272,7 @@ async function submitWithRetry( for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const signedTx = Transaction.fromCBORHex(signedTxCbor); - const txHash = await client.submitTx(signedTx); + const txHash = await sdkClient.submitTx(signedTx); console.log(`Success on attempt ${attempt}:`, txHash); return { success: true as const, txHash }; } catch (error: any) { @@ -329,16 +305,13 @@ async function submitWithRetry( Track transaction status with periodic checks: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function monitorTransaction( txHashHex: string, @@ -350,7 +323,7 @@ async function monitorTransaction( while (Date.now() - startTime < timeout) { try { - const confirmed = await client.awaitTx(txHash, checkInterval); + const confirmed = await sdkClient.awaitTx(txHash, checkInterval); if (confirmed) { console.log("Transaction confirmed:", txHashHex); @@ -373,16 +346,13 @@ async function monitorTransaction( End-to-end transaction submission with all error handling: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface TransactionStatus { status: "submitted" | "confirmed" | "failed"; @@ -404,12 +374,12 @@ export async function submitTransaction( try { const signedTx = Transaction.fromCBORHex(signedTxCbor); - const txHash = await client.submitTx(signedTx); + const txHash = await sdkClient.submitTx(signedTx); console.log(`Submitted (attempt ${attempts}):`, txHash); // Wait for confirmation - const confirmed = await client.awaitTx(txHash, 5000); + const confirmed = await sdkClient.awaitTx(txHash, 5000); if (confirmed) { return { @@ -462,22 +432,19 @@ export async function submitTransaction( Validate scripts before submitting: ```typescript twoslash -import { Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Transaction, TransactionHash, client, mainnet, blockfrost } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); async function evaluateAndSubmit(signedTxCbor: string) { try { // Evaluate first const signedTx = Transaction.fromCBORHex(signedTxCbor); - const redeemers = await client.evaluateTx(signedTx); + const redeemers = await sdkClient.evaluateTx(signedTx); console.log("Script evaluation:"); redeemers.forEach((r, i) => { @@ -489,8 +456,8 @@ async function evaluateAndSubmit(signedTxCbor: string) { }); // Submit if evaluation succeeds - const txHash = await client.submitTx(signedTx); - const confirmed = await client.awaitTx(txHash); + const txHash = await sdkClient.submitTx(signedTx); + const confirmed = await sdkClient.awaitTx(txHash); return { success: true, diff --git a/docs/content/docs/providers/use-cases.mdx b/docs/content/docs/providers/use-cases.mdx index d6a73690..2a07bdd5 100644 --- a/docs/content/docs/providers/use-cases.mdx +++ b/docs/content/docs/providers/use-cases.mdx @@ -12,16 +12,13 @@ Practical patterns and complete examples showing how to use providers in real ap Query and display address information: ```typescript twoslash -import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface AddressInfo { address: string; @@ -33,7 +30,7 @@ interface AddressInfo { export async function exploreAddress( addressBech32: string ): Promise { - const utxos = await client.getUtxos( + const utxos = await sdkClient.getUtxos( Address.fromBech32(addressBech32) ); @@ -56,16 +53,13 @@ export async function exploreAddress( Track balances across multiple addresses: ```typescript twoslash -import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface WalletBalance { address: string; @@ -87,7 +81,7 @@ export async function trackPortfolio( ): Promise { const wallets = await Promise.all( addressesBech32.map(async (addressBech32) => { - const utxos = await client.getUtxos( + const utxos = await sdkClient.getUtxos( Address.fromBech32(addressBech32) ); @@ -132,16 +126,13 @@ export async function trackPortfolio( Backend API endpoint for transaction submission: ```typescript twoslash -import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface SubmissionResponse { success: boolean; @@ -156,12 +147,12 @@ export async function handleSubmission( try { // Submit transaction const signedTx = Transaction.fromCBORHex(signedTxCbor); - const txHash = await client.submitTx(signedTx); + const txHash = await sdkClient.submitTx(signedTx); console.log("Transaction submitted:", txHash); // Wait for confirmation - const confirmed = await client.awaitTx(txHash, 5000); + const confirmed = await sdkClient.awaitTx(txHash, 5000); return { success: true, @@ -211,16 +202,13 @@ app.post("/api/submit", async (req, res) => { Track token distribution across holders: ```typescript twoslash -import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface TokenHolder { address: string; @@ -243,7 +231,7 @@ export async function trackDistribution( for (const addressBech32 of addresses) { const address = Address.fromBech32(addressBech32); - const utxos = await client.getUtxosWithUnit(address, tokenUnit); + const utxos = await sdkClient.getUtxosWithUnit(address, tokenUnit); if (utxos.length > 0) { // Count UTxOs that contain this token @@ -277,16 +265,13 @@ export async function trackDistribution( Track NFT ownership and rarity: ```typescript twoslash -import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface NFTOwnership { tokenId: string; @@ -305,7 +290,7 @@ export async function trackNFTCollection( const unit = policyId + tokenName; try { - const utxo = await client.getUtxoByUnit(unit); + const utxo = await sdkClient.getUtxoByUnit(unit); return { tokenId: tokenName, @@ -357,16 +342,13 @@ export async function getCollectionStats( Track delegators to a specific stake pool: ```typescript twoslash -import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface Delegator { rewardAddress: string; @@ -381,7 +363,7 @@ export async function trackPoolDelegators( const delegators = await Promise.all( rewardAddresses.map(async (rewardAddress) => { try { - const delegation = await client.getDelegation(RewardAddress.RewardAddress.make(rewardAddress)); + const delegation = await sdkClient.getDelegation(RewardAddress.RewardAddress.make(rewardAddress)); return { rewardAddress, @@ -428,16 +410,13 @@ export async function getPoolStats( Monitor network parameters for changes: ```typescript twoslash -import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "mainnet", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", +const sdkClient = client(mainnet) + .with(blockfrost({ +baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_PROJECT_ID! - } -}); +})); interface ParameterSnapshot { timestamp: Date; @@ -449,7 +428,7 @@ interface ParameterSnapshot { } export async function captureParameters(): Promise { - const params = await client.getProtocolParameters(); + const params = await sdkClient.getProtocolParameters(); return { timestamp: new Date(), @@ -514,7 +493,7 @@ export class ParameterMonitor { Implement provider failover for reliability: ```typescript twoslash -import { Address, PoolKeyHash, RewardAddress, Transaction, TransactionHash, createClient } from "@evolution-sdk/evolution"; +import { Address, Chain, PoolKeyHash, RewardAddress, Transaction, TransactionHash, client, mainnet, blockfrost, kupmios, koios, maestro } from "@evolution-sdk/evolution"; // Type for clients with common provider methods interface ProviderClient { @@ -529,7 +508,7 @@ class ProviderWithFallback { constructor( configs: Array<{ - network: "mainnet" | "preprod" | "preview"; + chain: Chain; provider: | { type: "blockfrost"; baseUrl: string; projectId: string } | { type: "maestro"; baseUrl: string; apiKey: string } @@ -537,9 +516,14 @@ class ProviderWithFallback { | { type: "koios"; baseUrl: string }; }> ) { - this.clients = configs.map(({ network, provider }) => - createClient({ network, provider }) - ); + this.clients = configs.map(({ chain, provider }) => { + const providerInstance = + provider.type === "blockfrost" ? blockfrost({ baseUrl: provider.baseUrl, projectId: provider.projectId }) + : provider.type === "maestro" ? maestro({ baseUrl: provider.baseUrl, apiKey: provider.apiKey }) + : provider.type === "kupmios" ? kupmios({ ogmiosUrl: provider.ogmiosUrl, kupoUrl: provider.kupoUrl }) + : koios({ baseUrl: provider.baseUrl }); + return client(chain).with(providerInstance); + }); } private async withFallback( @@ -566,16 +550,16 @@ class ProviderWithFallback { } async getUtxos(address: Address.Address) { - return this.withFallback((client) => client.getUtxos(address)); + return this.withFallback((sdkClient) => sdkClient.getUtxos(address)); } async submitTx(tx: Transaction.Transaction) { - return this.withFallback((client) => client.submitTx(tx)); + return this.withFallback((sdkClient) => sdkClient.submitTx(tx)); } async getProtocolParameters() { - return this.withFallback((client) => - client.getProtocolParameters() + return this.withFallback((sdkClient) => + sdkClient.getProtocolParameters() ); } } @@ -583,7 +567,7 @@ class ProviderWithFallback { // Usage const resilientProvider = new ProviderWithFallback([ { - network: "mainnet", + chain: mainnet, provider: { type: "blockfrost", baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0", @@ -591,7 +575,7 @@ const resilientProvider = new ProviderWithFallback([ } }, { - network: "mainnet", + chain: mainnet, provider: { type: "maestro", baseUrl: "https://mainnet.gomaestro-api.org/v1", diff --git a/docs/content/docs/querying/datums.mdx b/docs/content/docs/querying/datums.mdx index 418c5997..05cf1d87 100644 --- a/docs/content/docs/querying/datums.mdx +++ b/docs/content/docs/querying/datums.mdx @@ -10,17 +10,19 @@ When a UTxO uses a datum hash (instead of an inline datum), the full datum must ## Fetch Datum by Hash ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const datumHash: any -const datum = await client.getDatum(datumHash) +const datum = await sdkClient.getDatum(datumHash) console.log("Datum:", datum) ``` diff --git a/docs/content/docs/querying/delegation.mdx b/docs/content/docs/querying/delegation.mdx index 4b5c53f2..202cffa2 100644 --- a/docs/content/docs/querying/delegation.mdx +++ b/docs/content/docs/querying/delegation.mdx @@ -12,15 +12,17 @@ Check which pool a stake credential is delegated to and how many rewards have ac The simplest way to check delegation for your connected wallet: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const delegation = await client.getWalletDelegation() +const delegation = await sdkClient.getWalletDelegation() console.log("Pool:", delegation.poolId) // null if not delegated console.log("Rewards:", delegation.rewards) // Accumulated lovelace @@ -31,17 +33,19 @@ console.log("Rewards:", delegation.rewards) // Accumulated lovelace For querying any address's delegation, use `getDelegation` with a reward address: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const rewardAddress: any -const delegation = await client.getDelegation(rewardAddress) +const delegation = await sdkClient.getDelegation(rewardAddress) console.log("Pool:", delegation.poolId) console.log("Rewards:", delegation.rewards) diff --git a/docs/content/docs/querying/index.mdx b/docs/content/docs/querying/index.mdx index c733698a..361c16e2 100644 --- a/docs/content/docs/querying/index.mdx +++ b/docs/content/docs/querying/index.mdx @@ -7,7 +7,7 @@ import { Card, Cards } from 'fumadocs-ui/components/card' # Querying -Evolution SDK provides a unified query interface across all providers (Blockfrost, Maestro, Koios, Kupo/Ogmios). Query UTxOs, delegation status, protocol parameters, datums, and transaction confirmations through your client. +Evolution SDK provides a unified query interface across all providers (Blockfrost, Maestro, Koios, Kupo/Ogmios). Query UTxOs, delegation status, protocol parameters, datums, and transaction confirmations through your client instance. ## Available Queries @@ -26,21 +26,23 @@ Evolution SDK provides a unified query interface across all providers (Blockfros ## Quick Example ```typescript twoslash -import { Address, createClient } from "@evolution-sdk/evolution" +import { Address, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) // Query wallet UTxOs -const utxos = await client.getWalletUtxos() +const utxos = await sdkClient.getWalletUtxos() console.log("Wallet has", utxos.length, "UTxOs") // Query specific address const addr = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63") -const addrUtxos = await client.getUtxos(addr) +const addrUtxos = await sdkClient.getUtxos(addr) ``` ## Next Steps diff --git a/docs/content/docs/querying/protocol-parameters.mdx b/docs/content/docs/querying/protocol-parameters.mdx index 43fa34c8..256e5eee 100644 --- a/docs/content/docs/querying/protocol-parameters.mdx +++ b/docs/content/docs/querying/protocol-parameters.mdx @@ -10,15 +10,17 @@ Protocol parameters define the network's rules — fee calculations, size limits ## Query Parameters ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const params = await client.getProtocolParameters() +const params = await sdkClient.getProtocolParameters() console.log("Min fee coefficient:", params.minFeeA) console.log("Min fee constant:", params.minFeeB) @@ -46,17 +48,19 @@ console.log("Pool deposit:", params.poolDeposit) You can provide custom protocol parameters when building transactions: ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const customParams: any -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), diff --git a/docs/content/docs/querying/transaction-status.mdx b/docs/content/docs/querying/transaction-status.mdx index 31cdea4c..11d661dc 100644 --- a/docs/content/docs/querying/transaction-status.mdx +++ b/docs/content/docs/querying/transaction-status.mdx @@ -10,15 +10,17 @@ After submitting a transaction, use `awaitTx` to wait for it to appear on-chain. ## Wait for Confirmation ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -30,7 +32,7 @@ const signed = await tx.sign() const txHash = await signed.submit() // Wait for confirmation (poll every 3 seconds) -const confirmed = await client.awaitTx(txHash, 3000) +const confirmed = await sdkClient.awaitTx(txHash, 3000) console.log("Confirmed:", confirmed) ``` diff --git a/docs/content/docs/querying/utxos.mdx b/docs/content/docs/querying/utxos.mdx index 258827a9..f17d716d 100644 --- a/docs/content/docs/querying/utxos.mdx +++ b/docs/content/docs/querying/utxos.mdx @@ -10,16 +10,18 @@ UTxOs (Unspent Transaction Outputs) represent available funds on the blockchain. ## By Address ```typescript twoslash -import { Address, createClient } from "@evolution-sdk/evolution" +import { Address, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const address = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63") -const utxos = await client.getUtxos(address) +const utxos = await sdkClient.getUtxos(address) for (const utxo of utxos) { console.log("UTxO:", utxo.transactionId, "#", utxo.index) @@ -32,15 +34,17 @@ for (const utxo of utxos) { Query all UTxOs belonging to your connected wallet: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const utxos = await client.getWalletUtxos() +const utxos = await sdkClient.getWalletUtxos() const totalLovelace = utxos.reduce((sum, u) => sum + u.assets.lovelace, 0n) console.log("Total balance:", totalLovelace, "lovelace") ``` @@ -50,22 +54,24 @@ console.log("Total balance:", totalLovelace, "lovelace") Find UTxOs containing a specific native token: ```typescript twoslash -import { Address, createClient } from "@evolution-sdk/evolution" +import { Address, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const address = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63") const unit = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0c9d8e7f6" // UTxOs at address containing this token -const utxos = await client.getUtxosWithUnit(address, unit) +const utxos = await sdkClient.getUtxosWithUnit(address, unit) // Find the single UTxO holding an NFT (unique token) -const nftUtxo = await client.getUtxoByUnit(unit) +const nftUtxo = await sdkClient.getUtxoByUnit(unit) ``` ## By Output Reference @@ -73,17 +79,19 @@ const nftUtxo = await client.getUtxoByUnit(unit) Fetch specific UTxOs by their transaction hash and output index: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const refs: any[] -const utxos = await client.getUtxosByOutRef(refs) +const utxos = await sdkClient.getUtxosByOutRef(refs) ``` ## Next Steps diff --git a/docs/content/docs/smart-contracts/datums.mdx b/docs/content/docs/smart-contracts/datums.mdx index c081a47a..c36878de 100644 --- a/docs/content/docs/smart-contracts/datums.mdx +++ b/docs/content/docs/smart-contracts/datums.mdx @@ -21,16 +21,18 @@ Evolution SDK supports two ways to attach datums to outputs: Inline datums embed the full data in the output. The spending transaction can read it directly without needing to provide the datum separately: ```typescript twoslash -import { Address, Assets, Data, InlineDatum, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Data, InlineDatum, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) // Simple inline datum -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu"), @@ -45,19 +47,21 @@ const tx = await client Datum hashes store only a 32-byte hash in the output. The full datum must be provided when spending: ```typescript -import { Address, Assets, Data, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Data, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) // Compute datum hash from PlutusData const datum = Data.constr(0n, [5000000n]) const datumHash = Data.hashData(datum) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu"), @@ -72,7 +76,7 @@ const tx = await client For production contracts, define your datum structure with TSchema to get compile-time type checking and automatic CBOR encoding: ```typescript twoslash -import { Bytes, Data, TSchema } from "@evolution-sdk/evolution" +import { Bytes, Data, TSchema, blockfrost, seedWallet, client } from "@evolution-sdk/evolution" // Define datum schema matching your validator const EscrowDatumSchema = TSchema.Struct({ @@ -102,7 +106,7 @@ const plutusData = EscrowDatumCodec.toData(datum) For datums with multiple possible shapes: ```typescript twoslash -import { Bytes, Data, TSchema } from "@evolution-sdk/evolution" +import { Bytes, Data, TSchema, blockfrost, seedWallet, client } from "@evolution-sdk/evolution" const OrderDatumSchema = TSchema.Variant({ Buy: { @@ -133,7 +137,7 @@ const sellOrder: OrderDatum = { For quick prototyping without schemas: ```typescript twoslash -import { Bytes, Data, Text } from "@evolution-sdk/evolution" +import { Bytes, Data, Text, blockfrost, seedWallet, client } from "@evolution-sdk/evolution" // Constructor with fields const datum = Data.constr(0n, [ @@ -151,17 +155,19 @@ const datum = Data.constr(0n, [ When querying UTxOs, inline datums are available directly on the UTxO object: ```typescript twoslash -import { Address, Data, TSchema, Bytes, createClient } from "@evolution-sdk/evolution" +import { Address, Data, TSchema, Bytes, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) // Query UTxOs at a script address const scriptAddress = Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu") -const utxos = await client.getUtxos(scriptAddress) +const utxos = await sdkClient.getUtxos(scriptAddress) for (const utxo of utxos) { if (utxo.datumOption) { diff --git a/docs/content/docs/smart-contracts/index.mdx b/docs/content/docs/smart-contracts/index.mdx index d505f2a1..6ce29476 100644 --- a/docs/content/docs/smart-contracts/index.mdx +++ b/docs/content/docs/smart-contracts/index.mdx @@ -25,16 +25,18 @@ Smart contract interaction in Cardano involves three concepts: **Locking** (sending funds to a script): ```typescript twoslash -import { Address, Assets, Data, InlineDatum, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Data, InlineDatum, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) // Lock 10 ADA to a script address with an inline datum -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu"), @@ -49,19 +51,21 @@ const hash = await signed.submit() **Spending** (unlocking funds from a script): ```typescript twoslash -import { Address, Assets, Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Address, Assets, Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const validatorScript: any // Spend from script with a redeemer -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, diff --git a/docs/content/docs/smart-contracts/locking.mdx b/docs/content/docs/smart-contracts/locking.mdx index aae6d8e8..63530db7 100644 --- a/docs/content/docs/smart-contracts/locking.mdx +++ b/docs/content/docs/smart-contracts/locking.mdx @@ -14,19 +14,21 @@ This is the first half of any smart contract interaction — you lock funds, the Send ADA to a script address with an inline datum: ```typescript twoslash -import { Address, Assets, Data, InlineDatum, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Data, InlineDatum, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const scriptAddress = Address.fromBech32( "addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu" ) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: scriptAddress, @@ -45,13 +47,15 @@ console.log("Locked funds at:", txHash) For real contracts, define your datum with TSchema for type safety: ```typescript twoslash -import { Address, Assets, Bytes, Data, InlineDatum, TSchema, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Bytes, Data, InlineDatum, TSchema, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) // Define escrow datum schema const EscrowDatumSchema = TSchema.Struct({ @@ -73,7 +77,7 @@ const scriptAddress = Address.fromBech32( "addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu" ) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: scriptAddress, @@ -91,13 +95,15 @@ const txHash = await signed.submit() Lock both ADA and native tokens to a script: ```typescript twoslash -import { Address, Assets, Data, InlineDatum, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Data, InlineDatum, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const scriptAddress = Address.fromBech32( "addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu" @@ -112,7 +118,7 @@ assets = Assets.addByHex( 100n ) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: scriptAddress, @@ -130,13 +136,15 @@ await signed.submit() Store a script on-chain alongside the locked funds. Other transactions can reference this script instead of including it directly: ```typescript twoslash -import { Address, Assets, Data, InlineDatum, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Data, InlineDatum, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const validatorScript: any @@ -144,7 +152,7 @@ const scriptAddress = Address.fromBech32( "addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu" ) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: scriptAddress, @@ -163,18 +171,20 @@ await signed.submit() Lock funds to multiple script addresses in a single transaction: ```typescript twoslash -import { Address, Assets, Data, InlineDatum, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, Data, InlineDatum, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const escrowAddress = Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu") const vestingAddress = Address.fromBech32("addr_test1wz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3pqsyu") -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: escrowAddress, diff --git a/docs/content/docs/smart-contracts/redeemers.mdx b/docs/content/docs/smart-contracts/redeemers.mdx index 9bb4fb8a..29c7640d 100644 --- a/docs/content/docs/smart-contracts/redeemers.mdx +++ b/docs/content/docs/smart-contracts/redeemers.mdx @@ -18,19 +18,21 @@ Cardano redeemers reference inputs by their sorted position in the transaction. The simplest mode — provide a direct `Data` value. Use this when your redeemer doesn't need to know its input index: ```typescript twoslash -import { Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const validatorScript: any // Static redeemer — the value is fixed -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, @@ -47,19 +49,21 @@ const tx = await client A callback that receives the input's final index after coin selection. Use this when the redeemer needs to encode its own position: ```typescript twoslash -import { Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const validatorScript: any // Self redeemer — callback receives { index, utxo } -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, @@ -85,19 +89,21 @@ interface IndexedInput { A callback that sees all specified inputs with their final indices. Use this when multiple script inputs need coordinated redeemer values: ```typescript twoslash -import { Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const orderUtxos: UTxO.UTxO[] declare const validatorScript: any // Batch redeemer — callback sees all specified inputs -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: orderUtxos, @@ -119,20 +125,22 @@ const tx = await client Redeemers also apply to minting policies. The same three modes work: ```typescript twoslash -import { Assets, Data, createClient } from "@evolution-sdk/evolution" +import { Assets, Data, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const mintingPolicyScript: any let assets = Assets.fromLovelace(2_000_000n) assets = Assets.addByHex(assets, "abc123def456abc123def456abc123def456abc123def456abc123de", "", 100n) -const tx = await client +const tx = await sdkClient .newTx() .mintAssets({ assets, @@ -147,7 +155,7 @@ const tx = await client Define redeemer schemas for compile-time validation: ```typescript twoslash -import { Bytes, Data, TSchema } from "@evolution-sdk/evolution" +import { Bytes, Data, TSchema, blockfrost, seedWallet, client } from "@evolution-sdk/evolution" const RedeemerSchema = TSchema.Variant({ Claim: {}, diff --git a/docs/content/docs/smart-contracts/reference-scripts.mdx b/docs/content/docs/smart-contracts/reference-scripts.mdx index 5c23ee7e..9bf4576d 100644 --- a/docs/content/docs/smart-contracts/reference-scripts.mdx +++ b/docs/content/docs/smart-contracts/reference-scripts.mdx @@ -18,20 +18,22 @@ Reference scripts (Plutus V2+) let you store a script on-chain in a UTxO and ref Store your validator script on-chain by including it in a UTxO output: ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const validatorScript: any // Store script in a UTxO (send to your own address or a permanent holder) -const myAddress = await client.address() +const myAddress = await sdkClient.getAddress() -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: myAddress, @@ -50,19 +52,21 @@ console.log("Reference script deployed:", txHash) Reference the deployed script UTxO when spending from the validator: ```typescript twoslash -import { Address, Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Address, Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const referenceScriptUtxo: UTxO.UTxO // Reference the UTxO containing the script instead of attaching it -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, @@ -93,20 +97,22 @@ The key difference: `readFrom` makes the UTxO available as a reference input wit Reference inputs aren't just for scripts — you can also read datums from UTxOs without consuming them: ```typescript twoslash -import { Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const oracleUtxo: UTxO.UTxO declare const refScriptUtxo: UTxO.UTxO // Read oracle data + reference script in same transaction -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, diff --git a/docs/content/docs/smart-contracts/spending.mdx b/docs/content/docs/smart-contracts/spending.mdx index 8ffd2e99..b016ac93 100644 --- a/docs/content/docs/smart-contracts/spending.mdx +++ b/docs/content/docs/smart-contracts/spending.mdx @@ -12,19 +12,21 @@ Spending from a script means consuming a UTxO locked at a script address by prov Use `collectFrom` to specify which script UTxOs to spend and what redeemer to provide: ```typescript twoslash -import { Address, Assets, Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Address, Assets, Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const validatorScript: any // Spend script UTxOs with a "Claim" redeemer -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, @@ -42,19 +44,21 @@ const txHash = await signed.submit() Many validators check that a specific key signed the transaction. Use `addSigner` to include the required signer: ```typescript twoslash -import { Address, Assets, Data, KeyHash, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Address, Assets, Data, KeyHash, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const validatorScript: any declare const myKeyHash: KeyHash.KeyHash -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, @@ -73,20 +77,22 @@ await signed.submit() For time-locked validators, set the transaction validity interval so the script can verify the current time: ```typescript twoslash -import { Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const validatorScript: any const now = BigInt(Date.now()) -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, @@ -108,13 +114,15 @@ await signed.submit() Collect from a script and send the unlocked funds to a recipient: ```typescript twoslash -import { Address, Assets, Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Address, Assets, Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptUtxos: UTxO.UTxO[] declare const validatorScript: any @@ -123,7 +131,7 @@ const beneficiary = Address.fromBech32( "addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63" ) -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos, @@ -167,18 +175,20 @@ redeemer: { Add labels to identify operations in error messages when debugging script failures: ```typescript twoslash -import { Data, createClient, type UTxO } from "@evolution-sdk/evolution" +import { Data, client, type UTxO, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const escrowUtxos: UTxO.UTxO[] declare const validatorScript: any -const tx = await client +const tx = await sdkClient .newTx() .collectFrom({ inputs: escrowUtxos, diff --git a/docs/content/docs/staking/delegation.mdx b/docs/content/docs/staking/delegation.mdx index 9a9ac5a0..34a60425 100644 --- a/docs/content/docs/staking/delegation.mdx +++ b/docs/content/docs/staking/delegation.mdx @@ -12,18 +12,20 @@ Once your stake credential is registered, you can delegate your stake to earn re Assign your stake to a stake pool to earn rewards: ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const poolKeyHash: any -const tx = await client +const tx = await sdkClient .newTx() .delegateToPool({ stakeCredential, poolKeyHash }) .build() @@ -37,19 +39,21 @@ await signed.submit() In the Conway era, delegate your governance voting power to a Delegated Representative: ```typescript twoslash -import { Credential, DRep, createClient } from "@evolution-sdk/evolution" +import { Credential, DRep, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const drepKeyHash: any // Delegate to a specific DRep -const tx = await client +const tx = await sdkClient .newTx() .delegateToDRep({ stakeCredential, @@ -66,18 +70,20 @@ await signed.submit() You can also delegate to built-in options: ```typescript twoslash -import { Credential, DRep, createClient } from "@evolution-sdk/evolution" +import { Credential, DRep, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential // Always abstain from voting -const tx1 = await client +const tx1 = await sdkClient .newTx() .delegateToDRep({ stakeCredential, @@ -86,7 +92,7 @@ const tx1 = await client .build() // Always vote no confidence -const tx2 = await client +const tx2 = await sdkClient .newTx() .delegateToDRep({ stakeCredential, @@ -100,19 +106,21 @@ const tx2 = await client Delegate to a pool and DRep in a single certificate: ```typescript twoslash -import { Credential, DRep, createClient } from "@evolution-sdk/evolution" +import { Credential, DRep, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const poolKeyHash: any declare const drepKeyHash: any -const tx = await client +const tx = await sdkClient .newTx() .delegateToPoolAndDRep({ stakeCredential, @@ -130,19 +138,21 @@ await signed.submit() For script-controlled stake credentials, provide a redeemer: ```typescript twoslash -import { Credential, Data, createClient } from "@evolution-sdk/evolution" +import { Credential, Data, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptStakeCredential: Credential.Credential declare const poolKeyHash: any declare const stakeScript: any -const tx = await client +const tx = await sdkClient .newTx() .delegateToPool({ stakeCredential: scriptStakeCredential, diff --git a/docs/content/docs/staking/deregistration.mdx b/docs/content/docs/staking/deregistration.mdx index 1463b130..3ba65bbe 100644 --- a/docs/content/docs/staking/deregistration.mdx +++ b/docs/content/docs/staking/deregistration.mdx @@ -10,17 +10,19 @@ Deregistering a stake credential removes it from the chain and refunds the depos ## Basic Deregistration ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential -const tx = await client +const tx = await sdkClient .newTx() .deregisterStake({ stakeCredential }) .build() @@ -34,18 +36,20 @@ await signed.submit() Best practice: withdraw rewards and deregister in the same transaction to avoid losing rewards: ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const rewardBalance: bigint -const tx = await client +const tx = await sdkClient .newTx() .withdraw({ stakeCredential, amount: rewardBalance }) .deregisterStake({ stakeCredential }) @@ -58,18 +62,20 @@ await signed.submit() ## Script-Controlled Deregistration ```typescript twoslash -import { Credential, Data, createClient } from "@evolution-sdk/evolution" +import { Credential, Data, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptStakeCredential: Credential.Credential declare const stakeScript: any -const tx = await client +const tx = await sdkClient .newTx() .deregisterStake({ stakeCredential: scriptStakeCredential, diff --git a/docs/content/docs/staking/index.mdx b/docs/content/docs/staking/index.mdx index f1688a78..10d43935 100644 --- a/docs/content/docs/staking/index.mdx +++ b/docs/content/docs/staking/index.mdx @@ -20,19 +20,21 @@ Evolution SDK provides complete staking operations — register stake credential ## Quick Example ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const poolKeyHash: any // Register and delegate in one transaction -const tx = await client +const tx = await sdkClient .newTx() .registerStake({ stakeCredential }) .delegateToPool({ stakeCredential, poolKeyHash }) diff --git a/docs/content/docs/staking/registration.mdx b/docs/content/docs/staking/registration.mdx index 66ffde22..16f05dc8 100644 --- a/docs/content/docs/staking/registration.mdx +++ b/docs/content/docs/staking/registration.mdx @@ -10,17 +10,19 @@ Before you can delegate or earn rewards, your stake credential must be registere ## Basic Registration ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential -const tx = await client +const tx = await sdkClient .newTx() .registerStake({ stakeCredential }) .build() @@ -36,20 +38,22 @@ The deposit amount is fetched automatically from protocol parameters. The Conway era introduced combined certificates that register and delegate in one step, saving a certificate fee: ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const poolKeyHash: any declare const drep: any // Register + delegate to pool in one certificate -const tx = await client +const tx = await sdkClient .newTx() .registerAndDelegateTo({ stakeCredential, @@ -64,20 +68,22 @@ await signed.submit() You can also combine registration with DRep delegation or both: ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential declare const poolKeyHash: any declare const drep: any // Register + delegate to both pool and DRep -const tx = await client +const tx = await sdkClient .newTx() .registerAndDelegateTo({ stakeCredential, @@ -95,18 +101,20 @@ await signed.submit() For stake credentials controlled by Plutus scripts, provide a redeemer: ```typescript twoslash -import { Credential, Data, createClient } from "@evolution-sdk/evolution" +import { Credential, Data, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptStakeCredential: Credential.Credential declare const stakeScript: any -const tx = await client +const tx = await sdkClient .newTx() .registerStake({ stakeCredential: scriptStakeCredential, diff --git a/docs/content/docs/staking/withdrawal.mdx b/docs/content/docs/staking/withdrawal.mdx index 54ebb1d6..7e8ed1bc 100644 --- a/docs/content/docs/staking/withdrawal.mdx +++ b/docs/content/docs/staking/withdrawal.mdx @@ -10,22 +10,24 @@ Staking rewards accumulate each epoch and must be explicitly withdrawn to your w ## Basic Withdrawal ```typescript twoslash -import { Credential, createClient } from "@evolution-sdk/evolution" +import { Credential, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const stakeCredential: Credential.Credential // Query current rewards via wallet -const delegation = await client.getWalletDelegation() +const delegation = await sdkClient.getWalletDelegation() console.log("Available rewards:", delegation.rewards, "lovelace") // Withdraw all rewards -const tx = await client +const tx = await sdkClient .newTx() .withdraw({ stakeCredential, @@ -42,19 +44,21 @@ await signed.submit() Use `amount: 0n` to trigger a stake validator without actually withdrawing rewards. This is the **coordinator pattern** used by some DeFi protocols: ```typescript twoslash -import { Credential, Data, createClient } from "@evolution-sdk/evolution" +import { Credential, Data, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptStakeCredential: Credential.Credential declare const stakeScript: any // Trigger the stake validator with zero withdrawal -const tx = await client +const tx = await sdkClient .newTx() .withdraw({ stakeCredential: scriptStakeCredential, @@ -74,19 +78,21 @@ This pattern is useful for validators that need to run stake-level checks as par ## Script-Controlled Withdrawal ```typescript twoslash -import { Credential, Data, createClient } from "@evolution-sdk/evolution" +import { Credential, Data, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const scriptStakeCredential: Credential.Credential declare const stakeScript: any declare const rewardAmount: bigint -const tx = await client +const tx = await sdkClient .newTx() .withdraw({ stakeCredential: scriptStakeCredential, diff --git a/docs/content/docs/testing/integration-tests.mdx b/docs/content/docs/testing/integration-tests.mdx index 9ca5395a..d8290872 100644 --- a/docs/content/docs/testing/integration-tests.mdx +++ b/docs/content/docs/testing/integration-tests.mdx @@ -12,22 +12,22 @@ Integration tests run against a local devnet cluster, validating the full transa ```typescript import { describe, it, beforeAll, afterAll, expect } from "vitest" import { Cluster, Config, Genesis } from "@evolution-sdk/devnet" -import { Address, Assets, createClient, type SigningClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, kupmios, seedWallet } from "@evolution-sdk/evolution" describe("Transaction Tests", () => { let cluster: Cluster.Cluster - let client: SigningClient + let sdkClient: ReturnType> let genesisConfig: any beforeAll(async () => { const mnemonic = "test test test test test test test test test test test test test test test test test test test test test test test sauce" - const wallet = createClient({ - network: 0, - wallet: { type: "seed", mnemonic, accountIndex: 0 } - }) + const wallet = client(Cluster.BOOTSTRAP_CHAIN) + .with(seedWallet({ +mnemonic, accountIndex: 0 +})) - const addressHex = Address.toHex(await wallet.address()) + const addressHex = Address.toHex(await wallet.getAddress()) genesisConfig = { ...Config.DEFAULT_SHELLEY_GENESIS, @@ -47,15 +47,14 @@ describe("Transaction Tests", () => { await Cluster.start(cluster) await new Promise(resolve => setTimeout(resolve, 8000)) - client = createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1442", + sdkClient = client(Cluster.getChain(cluster)) + .with(kupmios({ +kupoUrl: "http://localhost:1442", ogmiosUrl: "http://localhost:1337" - }, - wallet: { type: "seed", mnemonic, accountIndex: 0 } - }) +})) + .with(seedWallet({ +mnemonic, accountIndex: 0 +})) }, 180_000) // Extended timeout for cluster startup afterAll(async () => { @@ -67,7 +66,7 @@ describe("Transaction Tests", () => { // Genesis UTxOs are NOT indexed by Kupo — must provide them explicitly const genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig) - const signBuilder = await client + const signBuilder = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae"), @@ -77,7 +76,7 @@ describe("Transaction Tests", () => { const submitBuilder = await signBuilder.sign() const txHash = await submitBuilder.submit() - const confirmed = await client.awaitTx(txHash, 1000) + const confirmed = await sdkClient.awaitTx(txHash, 1000) expect(confirmed).toBe(true) }, 30_000) diff --git a/docs/content/docs/time/index.mdx b/docs/content/docs/time/index.mdx index 6f747ca0..b9ed3aab 100644 --- a/docs/content/docs/time/index.mdx +++ b/docs/content/docs/time/index.mdx @@ -23,20 +23,22 @@ Cardano uses a slot-based time system. Each slot has a fixed duration (typically When you call `.setValidity({ from, to })`, you provide Unix timestamps in milliseconds. The transaction builder converts these to slots using the network's slot configuration: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) declare const tx: any const now = BigInt(Date.now()) // Set validity: valid from now, expires in 5 minutes -// await client.newTx() +// await sdkClient.newTx() // .setValidity({ from: now, to: now + 300_000n }) // ... ``` diff --git a/docs/content/docs/time/posix.mdx b/docs/content/docs/time/posix.mdx index dda90ff6..16c528b4 100644 --- a/docs/content/docs/time/posix.mdx +++ b/docs/content/docs/time/posix.mdx @@ -36,18 +36,20 @@ const expiresIn1Hour = now + oneHour Time values are passed to `.setValidity()` as Unix milliseconds. The builder converts them to slots automatically: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution" +import { client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const now = BigInt(Date.now()) // Transaction valid for the next 10 minutes -// const tx = await client.newTx() +// const tx = await sdkClient.newTx() // .setValidity({ from: now, to: now + 600_000n }) // ... ``` diff --git a/docs/content/docs/time/slots.mdx b/docs/content/docs/time/slots.mdx index d90c6e2d..5eb51136 100644 --- a/docs/content/docs/time/slots.mdx +++ b/docs/content/docs/time/slots.mdx @@ -30,15 +30,17 @@ The builder uses these to convert: `slot = zeroSlot + (unixTime - zeroTime) / sl For devnet or custom networks, you can override the slot config in build options: ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), diff --git a/docs/content/docs/time/validity-ranges.mdx b/docs/content/docs/time/validity-ranges.mdx index 5e021425..ecd03dcd 100644 --- a/docs/content/docs/time/validity-ranges.mdx +++ b/docs/content/docs/time/validity-ranges.mdx @@ -12,17 +12,19 @@ Validity ranges define the time window during which a transaction can be include Use `.setValidity()` with Unix timestamps in milliseconds: ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const now = BigInt(Date.now()) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), diff --git a/docs/content/docs/transactions/chaining.mdx b/docs/content/docs/transactions/chaining.mdx index 37ec37a9..f7c9a443 100644 --- a/docs/content/docs/transactions/chaining.mdx +++ b/docs/content/docs/transactions/chaining.mdx @@ -47,25 +47,27 @@ Transactions must be **submitted in order**. Each transaction spends outputs cre The simplest case: two payments built back-to-back, submitted in order. ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const alice = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63") const bob = Address.fromBech32("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae") // Build first transaction — auto-fetches wallet UTxOs -const tx1 = await client +const tx1 = await sdkClient .newTx() .payToAddress({ address: alice, assets: Assets.fromLovelace(2_000_000n) }) .build() // Build second transaction immediately — no waiting for tx1 to confirm -const tx2 = await client +const tx2 = await sdkClient .newTx() .payToAddress({ address: bob, assets: Assets.fromLovelace(2_000_000n) }) .build({ availableUtxos: tx1.chainResult().available }) @@ -83,19 +85,21 @@ await signed2.submit() Use `tx1.chainResult().available` to find the output you want to spend in tx2. ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const alice = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63") const bob = Address.fromBech32("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae") // tx1 sends 5 ADA to Alice -const tx1 = await client +const tx1 = await sdkClient .newTx() .payToAddress({ address: alice, assets: Assets.fromLovelace(5_000_000n) }) .build() @@ -109,7 +113,7 @@ const aliceOutput = chain1.available.find( )! // tx2 immediately spends Alice's output, forwarding to Bob -const tx2 = await client +const tx2 = await sdkClient .newTx() .collectFrom({ inputs: [aliceOutput] }) .payToAddress({ address: bob, assets: Assets.fromLovelace(4_500_000n) }) @@ -125,13 +129,15 @@ await (await tx2.sign()).submit() Chain three builds together up-front, then submit all three. ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) const recipients = [ Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -139,17 +145,17 @@ const recipients = [ Address.fromBech32("addr_test1qpq6xvp5y4fw0wfgxfqmn78qqagkpv4q7qpqyz8s8x3snp5n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgsc3z7t3"), ] -const tx1 = await client +const tx1 = await sdkClient .newTx() .payToAddress({ address: recipients[0], assets: Assets.fromLovelace(5_000_000n) }) .build() -const tx2 = await client +const tx2 = await sdkClient .newTx() .payToAddress({ address: recipients[1], assets: Assets.fromLovelace(5_000_000n) }) .build({ availableUtxos: tx1.chainResult().available }) -const tx3 = await client +const tx3 = await sdkClient .newTx() .payToAddress({ address: recipients[2], assets: Assets.fromLovelace(5_000_000n) }) .build({ availableUtxos: tx2.chainResult().available }) diff --git a/docs/content/docs/transactions/first-transaction.mdx b/docs/content/docs/transactions/first-transaction.mdx index f1d93808..3a47b370 100644 --- a/docs/content/docs/transactions/first-transaction.mdx +++ b/docs/content/docs/transactions/first-transaction.mdx @@ -14,25 +14,21 @@ This guide walks through a complete payment transaction from client setup to on- Here's a full transaction workflow—configure once, then build, sign, and submit: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; // 1. Configure your client -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 - } -}); +})); // 2. Build transaction -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -54,21 +50,17 @@ console.log("Transaction submitted:", txHash); Set up your connection to the network. This happens once—you'll reuse the client throughout your application: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 - } -}); +})); ``` ### Stage 2: Building the Transaction @@ -76,15 +68,17 @@ const client = createClient({ Chain operations to specify what the transaction should do. Call `.build()` when ready to finalize: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}); +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})); -const tx = await client.newTx() +const tx = await sdkClient.newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), assets: Assets.fromLovelace(2_000_000n) @@ -99,15 +93,17 @@ The builder handles UTxO selection, fee calculation, and change outputs automati Authorize the transaction with your wallet's private keys: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}); +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})); -const tx = await client.newTx() +const tx = await sdkClient.newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), assets: Assets.fromLovelace(2_000_000n) }) .build(); @@ -119,15 +115,17 @@ const signed = await tx.sign(); Broadcast the signed transaction to the blockchain and get the transaction hash: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}); +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})); -const tx = await client.newTx() +const tx = await sdkClient.newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), assets: Assets.fromLovelace(2_000_000n) }) .build(); diff --git a/docs/content/docs/transactions/multi-output.mdx b/docs/content/docs/transactions/multi-output.mdx index e4e6ff32..1da6d085 100644 --- a/docs/content/docs/transactions/multi-output.mdx +++ b/docs/content/docs/transactions/multi-output.mdx @@ -10,15 +10,17 @@ Chain multiple `.payToAddress()` calls to send to several recipients in a single ## Multiple Recipients ```typescript twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution" +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -43,15 +45,17 @@ await signed.submit() Drain your entire wallet to a single address using `sendAll`: ```typescript twoslash -import { Address, createClient } from "@evolution-sdk/evolution" +import { Address, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution" -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}) +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})) -const tx = await client +const tx = await sdkClient .newTx() .sendAll({ to: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63") diff --git a/docs/content/docs/transactions/retry-safe.mdx b/docs/content/docs/transactions/retry-safe.mdx index 594ac37a..ef501ee1 100644 --- a/docs/content/docs/transactions/retry-safe.mdx +++ b/docs/content/docs/transactions/retry-safe.mdx @@ -42,23 +42,22 @@ Querying chain state **outside** the action and passing it in as a static value The simplest approach: wrap the full pipeline in an async function and call it from a retry loop. ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}); +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})); const recipient = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"); // The action fetches UTxOs at call time — safe to retry async function sendPayment() { - const tx = await client + const tx = await sdkClient .newTx() .payToAddress({ address: recipient, assets: Assets.fromLovelace(2_000_000n) }) .build(); @@ -89,17 +88,16 @@ console.log("Submitted:", txHash); When collecting from a script address, query the script UTxOs inside the action so each retry gets a fresh view of what is available at that address. ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}); +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})); // Illustrative snippet (not runnable as-is) — redeemer and scriptAddress are placeholders async function unlockFromScript() { @@ -107,9 +105,9 @@ async function unlockFromScript() { const recipient = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"); // Script UTxOs fetched inside the action — re-run on every retry - const scriptUtxos = await client.getUtxos(scriptAddress); + const scriptUtxos = await sdkClient.getUtxos(scriptAddress); - const tx = await client + const tx = await sdkClient .newTx() .collectFrom({ inputs: scriptUtxos }) .payToAddress({ address: recipient, assets: Assets.fromLovelace(5_000_000n) }) @@ -125,25 +123,24 @@ async function unlockFromScript() { When using Effect, compose the full pipeline as a single `Effect.gen` and apply `Effect.retry` directly. `Schedule` controls the timing and number of attempts. ```ts twoslash -import { Address, createClient } from "@evolution-sdk/evolution"; +import { Address, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; import { Effect, Schedule } from "effect"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { type: "seed", mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 } -}); +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 +})); // Illustrative snippet (not runnable as-is) — scriptAddress and redeemer are placeholders const unlockAction = Effect.gen(function* () { // Script UTxOs fetched fresh on every attempt - const scriptUtxos = yield* client.Effect.getUtxos(Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63")); + const scriptUtxos = yield* sdkClient.Effect.getUtxos(Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63")); - const signBuilder = yield* client.newTx() + const signBuilder = yield* sdkClient.newTx() .collectFrom({ inputs: scriptUtxos }) .buildEffect(); diff --git a/docs/content/docs/transactions/simple-payment.mdx b/docs/content/docs/transactions/simple-payment.mdx index 70e07a88..d2c09073 100644 --- a/docs/content/docs/transactions/simple-payment.mdx +++ b/docs/content/docs/transactions/simple-payment.mdx @@ -14,24 +14,20 @@ This guide covers common payment patterns—from basic ADA transfers to payments The simplest transaction sends only lovelace (ADA's smallest unit). 1 ADA = 1,000,000 lovelace: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 - } -}); +})); // Send 2 ADA -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -66,21 +62,17 @@ console.log(lovelace); // 2500000n Include native tokens (custom tokens or NFTs) alongside ADA. The `assets` object takes any asset by its policy ID + asset name: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! - }, - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +})) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 - } -}); +})); // Send 2 ADA plus 100 tokens const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0c9d8e7f6"; @@ -88,7 +80,7 @@ const assetName = ""; // empty for fungible tokens let assets = Assets.fromLovelace(2_000_000n); assets = Assets.addByHex(assets, policyId, assetName, 100n); -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), @@ -109,19 +101,21 @@ The policy ID + asset name is concatenated into a single hex string. Make your code more readable with descriptive variable names: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}); +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})); // ---cut--- const recipientAddress = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"); const paymentAmount = 5_000_000n; // 5 ADA -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: recipientAddress, @@ -135,19 +129,21 @@ const tx = await client Adjust transaction values based on network or configuration: ```ts twoslash -import { Address, Assets, createClient } from "@evolution-sdk/evolution"; +import { Address, Assets, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - provider: { type: "blockfrost", baseUrl: "", projectId: "" }, - wallet: { type: "seed", mnemonic: "", accountIndex: 0 } -}); +const sdkClient = client(preprod) + .with(blockfrost({ +baseUrl: "", projectId: "" +})) + .with(seedWallet({ +mnemonic: "", accountIndex: 0 +})); // ---cut--- const isMainnet = process.env.NETWORK === "mainnet"; const amount = isMainnet ? 5_000_000n : 1_000_000n; -const tx = await client +const tx = await sdkClient .newTx() .payToAddress({ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), diff --git a/docs/content/docs/wallets/api-wallet.mdx b/docs/content/docs/wallets/api-wallet.mdx index fa4f6224..5cba02ed 100644 --- a/docs/content/docs/wallets/api-wallet.mdx +++ b/docs/content/docs/wallets/api-wallet.mdx @@ -39,7 +39,7 @@ Never request or store user keys. Frontend signs only and should not include pro Frontend creates API wallet client without provider. Can sign and submit transactions but cannot build them. ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, cip30Wallet } from "@evolution-sdk/evolution"; declare const cardano: any; // window.cardano @@ -48,23 +48,21 @@ async function signAndSubmit() { const walletApi = await cardano.eternl.enable(); // 2. Create API wallet client (no provider needed for signing/submitting) - const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } - }); + const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); // 3. Get user address - const userAddress = await client.address(); + const userAddress = await sdkClient.getAddress(); console.log("User address:", userAddress); // 4. Request backend to build transaction (backend has provider + read-only wallet) const unsignedTxCbor = "84a400..."; // From backend // 5. Sign with user's wallet (prompts user approval) - const witnessSet = await client.signTx(unsignedTxCbor); + const witnessSet = await sdkClient.signTx(unsignedTxCbor); - // 6. Submit directly through wallet API (CIP-30 wallets can submit) - const txHash = await client.submitTx(unsignedTxCbor); + // 6. Submit via wallet API (CIP-30 wallets can submit) + const txHash = await sdkClient.walletSubmitTx(unsignedTxCbor); console.log("Transaction submitted:", txHash); } ``` @@ -88,7 +86,7 @@ interface ApiWalletConfig { ## Complete dApp Example ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, cip30Wallet } from "@evolution-sdk/evolution"; declare const cardano: any; @@ -106,13 +104,11 @@ async function connectAndPay() { const walletApi = await cardano[walletName].enable(); // Create API wallet client - const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } - }); + const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); // Get user address - const address = await client.address(); + const address = await sdkClient.getAddress(); // Send address to backend for transaction building const response = await fetch("/api/build-payment", { @@ -128,10 +124,10 @@ async function connectAndPay() { const { txCbor } = await response.json(); // Sign with user wallet (prompts user for approval) - const witnessSet = await client.signTx(txCbor); + const witnessSet = await sdkClient.signTx(txCbor); - // Submit directly via wallet API (CIP-30 wallets can submit) - const txHash = await client.submitTx(txCbor); + // Submit via wallet API (CIP-30 wallets can submit) + const txHash = await sdkClient.walletSubmitTx(txCbor); console.log("Payment sent:", txHash); } ``` @@ -141,7 +137,7 @@ async function connectAndPay() { Hardware wallets work through browser extensions. The extension communicates with the hardware device. ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, cip30Wallet } from "@evolution-sdk/evolution"; declare const cardano: any; @@ -150,14 +146,12 @@ async function useHardwareWallet() { const walletApi = await cardano.eternl.enable(); // Extension handles hardware // Create API wallet client (same as software wallets) - const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } - }); + const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); // Extension will prompt hardware wallet for signatures const txCbor = "84a400..."; // Transaction CBOR from backend - const witnessSet = await client.signTx(txCbor); + const witnessSet = await sdkClient.signTx(txCbor); } ``` @@ -182,7 +176,7 @@ Hardware wallets and extensions follow Cardano's BIP-32 derivation paths: ## Error Handling ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, cip30Wallet } from "@evolution-sdk/evolution"; declare const cardano: any; @@ -197,12 +191,10 @@ async function connectWallet(walletName: string) { const walletApi = await cardano[walletName].enable(); // Create API wallet client - const client = createClient({ - network: "mainnet", - wallet: { type: "api", api: walletApi } - }); + const sdkClient = client(mainnet) + .with(cip30Wallet(walletApi)); - return client; + return sdkClient; } catch (error: any) { if (error.code === 2) { diff --git a/docs/content/docs/wallets/index.mdx b/docs/content/docs/wallets/index.mdx index 8a8d0484..f9ed3946 100644 --- a/docs/content/docs/wallets/index.mdx +++ b/docs/content/docs/wallets/index.mdx @@ -44,7 +44,7 @@ Uses direct extended private key material without mnemonic derivation. ```typescript { type: "private-key", - key: "xprv..." + paymentKey: "xprv..." } ``` diff --git a/docs/content/docs/wallets/private-key.mdx b/docs/content/docs/wallets/private-key.mdx index 20346cec..8ae37431 100644 --- a/docs/content/docs/wallets/private-key.mdx +++ b/docs/content/docs/wallets/private-key.mdx @@ -39,24 +39,23 @@ Never log or transmit keys in plaintext. Never store in environment variables as ## Basic Setup ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; - -// Create signing-only client with private key (no provider) -// Can sign transactions but cannot query blockchain or submit -const client = createClient({ - network: "preprod", - wallet: { - type: "private-key", - paymentKey: process.env.PAYMENT_SIGNING_KEY! // 192 hex chars - // Optional: stakeKey for staking operations - } -}); - -const address = await client.address(); -console.log("Payment address:", address); - -// To query blockchain or submit, attach a provider: -// const fullClient = client.attachProvider({ type: "blockfrost", ... }); +import { client, preprod, blockfrost, privateKeyWallet } from "@evolution-sdk/evolution"; + +async function example() { + // Create signing-only client with private key (no provider) + // Can sign transactions but cannot query blockchain or submit + const sdkClient = client(preprod) + .with(privateKeyWallet({ + paymentKey: process.env.PAYMENT_SIGNING_KEY! // 192 hex chars + // Optional: stakeKey for staking operations + })); + + const address = await sdkClient.getAddress(); + console.log("Payment address:", address); + + // To query blockchain or submit, attach a provider: + // const fullClient = sdkClient.with(blockfrost({ ... })); +} ``` ## Configuration Options @@ -74,7 +73,7 @@ interface PrivateKeyWalletConfig { Load keys from secure secret management systems: ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, blockfrost, privateKeyWallet } from "@evolution-sdk/evolution"; // Mock vault client for documentation purposes declare const secretsManager: { @@ -87,13 +86,10 @@ async function createSecureClient() { // Load from secure vault (AWS Secrets Manager, Azure Key Vault, etc.) const signingKey = await loadFromSecureVault("cardano-payment-key"); - return createClient({ - network: "mainnet", - wallet: { - type: "private-key", - paymentKey: signingKey - } - }); + return client(mainnet) + .with(privateKeyWallet({ +paymentKey: signingKey +})); } // Example vault integration pattern @@ -128,7 +124,7 @@ Log all key access events without logging the keys themselves. Alert on unusual ## AWS Secrets Manager Example ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, blockfrost, privateKeyWallet } from "@evolution-sdk/evolution"; // Mock AWS SDK classes for documentation declare class SecretsManagerClient { @@ -152,20 +148,17 @@ async function createProductionClient() { const secret = JSON.parse(response.SecretString!); // Create client with retrieved key - return createClient({ - network: "mainnet", - wallet: { - type: "private-key", - paymentKey: secret.paymentKey - } - }); + return client(mainnet) + .with(privateKeyWallet({ +paymentKey: secret.paymentKey +})); } ``` ## Azure Key Vault Example ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, blockfrost, privateKeyWallet } from "@evolution-sdk/evolution"; // Mock Azure SDK classes for documentation declare class DefaultAzureCredential {} @@ -183,20 +176,17 @@ async function createProductionClient() { // Retrieve secret const secret = await secretClient.getSecret("cardano-payment-key"); - return createClient({ - network: "mainnet", - wallet: { - type: "private-key", - paymentKey: secret.value! - } - }); + return client(mainnet) + .with(privateKeyWallet({ +paymentKey: secret.value! +})); } ``` ## Key Rotation Strategy ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; +import { client, mainnet, blockfrost, privateKeyWallet } from "@evolution-sdk/evolution"; async function rotateKeys() { // 1. Generate new key (offline, secure environment) @@ -206,16 +196,13 @@ async function rotateKeys() { await storeInVault("cardano-payment-key-new", newKey); // 3. Create client with new key - const client = createClient({ - network: "mainnet", - wallet: { - type: "private-key", - paymentKey: await loadFromVault("cardano-payment-key-new") - } - }); + const sdkClient = client(mainnet) + .with(privateKeyWallet({ +paymentKey: await loadFromVault("cardano-payment-key-new") +})); // 4. Monitor for issues - await monitorTransactions(client); + await monitorTransactions(sdkClient); // 5. After confirmation period, promote new key await promoteKey("cardano-payment-key-new", "cardano-payment-key"); @@ -235,34 +222,29 @@ declare function archiveKey(id: string): Promise; ## Environment Separation ```typescript twoslash -import { createClient } from "@evolution-sdk/evolution"; - -// Development -const devClient = createClient({ - network: "preprod", - wallet: { - type: "private-key", - paymentKey: await loadFromVault("dev/cardano-payment-key") - } -}); - -// Staging -const stagingClient = createClient({ - network: "preprod", // Still testnet - wallet: { - type: "private-key", - paymentKey: await loadFromVault("staging/cardano-payment-key") // Different key - } -}); - -// Production -const prodClient = createClient({ - network: "mainnet", - wallet: { - type: "private-key", - paymentKey: await loadFromVault("prod/cardano-payment-key") // Different key - } -}); +import { client, preprod, mainnet, blockfrost, privateKeyWallet } from "@evolution-sdk/evolution"; + +async function createEnvironmentClients() { + // Development + const devClient = client(preprod) + .with(privateKeyWallet({ + paymentKey: await loadFromVault("dev/cardano-payment-key") + })); + + // Staging + const stagingClient = client(preprod) + .with(privateKeyWallet({ + paymentKey: await loadFromVault("staging/cardano-payment-key") // Different key + })); + + // Production + const prodClient = client(mainnet) + .with(privateKeyWallet({ + paymentKey: await loadFromVault("prod/cardano-payment-key") // Different key + })); + + return { devClient, stagingClient, prodClient }; +} declare function loadFromVault(id: string): Promise; ``` diff --git a/docs/content/docs/wallets/security.mdx b/docs/content/docs/wallets/security.mdx index 5594bac4..bcacdd91 100644 --- a/docs/content/docs/wallets/security.mdx +++ b/docs/content/docs/wallets/security.mdx @@ -38,7 +38,7 @@ logger.debug({ mnemonic: process.env.MNEMONIC }); const badConfig = { wallet: { type: "private-key", - bech32PrivateKey: process.env.PRIVATE_KEY! // Bundled to client (DON'T DO THIS) + paymentKey: process.env.PRIVATE_KEY! // Bundled to client (DON'T DO THIS) } }; ``` @@ -160,7 +160,7 @@ BLOCKFROST_PROJECT_ID=preprodxxxxxxxxxxxx // backend code (see /docs/clients for client creation examples). const providerConfig = { - network: process.env.NETWORK as "preprod" | "mainnet", + chain: process.env.NETWORK === "preprod" ? preprod : mainnet, provider: { type: "blockfrost", baseUrl: process.env.NETWORK === "preprod" @@ -267,7 +267,7 @@ MNEMONIC="staging staging staging..." ```typescript // Error: Always uses same address -const address = await client.address(); +const address = await client.getAddress(); // Use for everything - bad for privacy ``` diff --git a/docs/content/docs/wallets/seed-phrase.mdx b/docs/content/docs/wallets/seed-phrase.mdx index f1d1c0a8..1d502482 100644 --- a/docs/content/docs/wallets/seed-phrase.mdx +++ b/docs/content/docs/wallets/seed-phrase.mdx @@ -36,7 +36,7 @@ Mnemonics provide human-friendly backup and recovery. The same 24 words reconstr Use the SDK to generate a cryptographically secure 24-word mnemonic: ```typescript twoslash -import { PrivateKey } from "@evolution-sdk/evolution"; +import { PrivateKey, blockfrost, seedWallet, client } from "@evolution-sdk/evolution"; // Generate 24-word mnemonic (256-bit entropy) const mnemonic = PrivateKey.generateMnemonic(); @@ -48,25 +48,22 @@ console.log(mnemonic); ## Basic Setup ```typescript twoslash -import { PrivateKey, createClient } from "@evolution-sdk/evolution"; +import { PrivateKey, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; // Create a signing-only client with seed wallet (no provider) // Can sign transactions but cannot query blockchain or submit -const client = createClient({ - network: "preprod", - wallet: { - type: "seed", - mnemonic: "fitness juice ankle box prepare gallery purse narrow miracle next soccer category analyst wait verb patch kit era hen clerk write skin trumpet attract", +const sdkClient = client(preprod) + .with(seedWallet({ +mnemonic: "fitness juice ankle box prepare gallery purse narrow miracle next soccer category analyst wait verb patch kit era hen clerk write skin trumpet attract", accountIndex: 0 // First account (increment for more accounts) - } -}); +})); // Get address (wallet operations work without provider) -const address = await client.address(); +const address = await sdkClient.getAddress(); console.log("Derived address:", address); // To query blockchain or submit, attach a provider: -// const fullClient = client.attachProvider({ type: "blockfrost", ... }); +// const fullClient = sdkClient.with(blockfrost({ baseUrl: "...", projectId: "..." })); ``` ## Configuration Options @@ -84,27 +81,21 @@ interface SeedWalletConfig { Different networks use the same wallet configuration—just change the network parameter: ```typescript twoslash -import { PrivateKey, createClient } from "@evolution-sdk/evolution"; +import { PrivateKey, client, preprod, mainnet, blockfrost, seedWallet } from "@evolution-sdk/evolution"; // Testnet client -const testClient = createClient({ - network: "preprod", - wallet: { - type: "seed", - mnemonic: "fitness juice ankle box prepare gallery purse narrow miracle next soccer category analyst wait verb patch kit era hen clerk write skin trumpet attract", +const testClient = client(preprod) + .with(seedWallet({ +mnemonic: "fitness juice ankle box prepare gallery purse narrow miracle next soccer category analyst wait verb patch kit era hen clerk write skin trumpet attract", accountIndex: 0 - } -}); +})); // Mainnet client (use DIFFERENT mnemonic in production!) -const mainClient = createClient({ - network: "mainnet", - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, // Different mnemonic! +const mainClient = client(mainnet) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, // Different mnemonic! accountIndex: 0 - } -}); +})); ``` ## Environment Variables @@ -116,16 +107,13 @@ BLOCKFROST_PROJECT_ID="preprodYourProjectIdHere" ``` ```typescript twoslash -import { PrivateKey, createClient } from "@evolution-sdk/evolution"; +import { PrivateKey, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; -const client = createClient({ - network: "preprod", - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, // From environment +const sdkClient = client(preprod) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, // From environment accountIndex: 0 - } -}); +})); ``` ## Multiple Accounts @@ -133,30 +121,24 @@ const client = createClient({ Generate independent wallets from the same mnemonic using different `accountIndex` values: ```typescript twoslash -import { PrivateKey, createClient } from "@evolution-sdk/evolution"; +import { PrivateKey, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; // Account 0 (default) -const account0 = createClient({ - network: "preprod", - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +const account0 = client(preprod) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 - } -}); +})); // Account 1 (different addresses, same mnemonic) -const account1 = createClient({ - network: "preprod", - wallet: { - type: "seed", - mnemonic: process.env.WALLET_MNEMONIC!, +const account1 = client(preprod) + .with(seedWallet({ +mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 1 - } -}); +})); -const addr0 = await account0.address(); -const addr1 = await account1.address(); +const addr0 = await account0.getAddress(); +const addr1 = await account1.getAddress(); console.log("Different addresses:", addr0 !== addr1); // true ``` @@ -165,7 +147,7 @@ console.log("Different addresses:", addr0 !== addr1); // true ### Development Testing ```typescript twoslash -import { PrivateKey, createClient } from "@evolution-sdk/evolution"; +import { PrivateKey, client, preprod, blockfrost, seedWallet } from "@evolution-sdk/evolution"; // Mock test runner functions for documentation declare function describe(name: string, fn: () => void): void; @@ -177,23 +159,20 @@ declare const expect: any; declare const process: { env: { TEST_WALLET_MNEMONIC?: string } }; describe("Payment tests", () => { - let client: any; + let sdkClient: any; beforeEach(() => { - client = createClient({ - network: "preprod", - wallet: { - type: "seed", - mnemonic: process.env.TEST_WALLET_MNEMONIC!, + sdkClient = client(preprod) + .with(seedWallet({ +mnemonic: process.env.TEST_WALLET_MNEMONIC!, accountIndex: 0 - } - }); +})); }); it("should sign transaction", async () => { // Wallet client can sign but needs provider to build/submit - // For full flow, attach provider: client.attachProvider(...) - const address = await client.address(); + // For full flow, attach provider: sdkClient.with(blockfrost({ baseUrl: "...", projectId: "..." })) + const address = await sdkClient.getAddress(); expect(address).toBeDefined(); }); }); diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts index 7a70f65a..9edff1c7 100644 --- a/docs/next-env.d.ts +++ b/docs/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts" +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/examples/with-vite-react/README.md b/examples/with-vite-react/README.md index 4c0fc473..b4b8c924 100644 --- a/examples/with-vite-react/README.md +++ b/examples/with-vite-react/README.md @@ -104,19 +104,18 @@ with-vite-react/ The app demonstrates how to use the Evolution SDK for building and submitting transactions: ```typescript -import { createClient } from "@evolution-sdk/evolution"; +import { client, preprod, blockfrost, cip30Wallet } from "@evolution-sdk/evolution"; // Create client with network -const client = createClient("preprod") - .attachWallet({ type: "api", api: walletApi }) - .attachProvider({ - type: "blockfrost", - baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", +const sdkClient = client(preprod) + .with(cip30Wallet(walletApi)) + .with(blockfrost({ +baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: "your_project_id" - }); +})); // Build and submit transaction -const txHash = await client +const txHash = await sdkClient .newTx() .payToAddress({ address: recipientAddress, diff --git a/examples/with-vite-react/src/components/TransactionBuilder.tsx b/examples/with-vite-react/src/components/TransactionBuilder.tsx index fa89e78d..f3ccbd30 100644 --- a/examples/with-vite-react/src/components/TransactionBuilder.tsx +++ b/examples/with-vite-react/src/components/TransactionBuilder.tsx @@ -1,7 +1,7 @@ import { useCardano } from "@cardano-foundation/cardano-connect-with-wallet" import { NetworkType } from "@cardano-foundation/cardano-connect-with-wallet-core" import { useState } from "react" -import { Address, Assets, createClient, TransactionHash } from "@evolution-sdk/evolution" +import { Address, Assets, client, mainnet, preprod, preview, TransactionHash, blockfrost, cip30Wallet } from "@evolution-sdk/evolution" export default function TransactionBuilder() { const [txHash, setTxHash] = useState(null) @@ -46,8 +46,12 @@ export default function TransactionBuilder() { throw new Error("Failed to enable wallet") } - // Determine network ID and provider config - const networkId = networkEnv // "preprod", "preview", or "mainnet" + // Determine chain from environment variable + const chainMap = { mainnet, preprod, preview } as const + const resolvedKey = (networkEnv as keyof typeof chainMap) in chainMap + ? (networkEnv as keyof typeof chainMap) + : "preprod" + const chain = chainMap[resolvedKey] // Configure Blockfrost provider based on network const blockfrostUrls = { @@ -56,18 +60,10 @@ export default function TransactionBuilder() { mainnet: "https://cardano-mainnet.blockfrost.io/api/v0" } - const providerConfig = { - type: "blockfrost" as const, - baseUrl: blockfrostUrls[networkId as keyof typeof blockfrostUrls], - projectId: import.meta.env.VITE_BLOCKFROST_PROJECT_ID || "" - } - // Create client with wallet and provider - const client = createClient({ - network: networkId, - provider: providerConfig, - wallet: { type: "api", api } - }) + const sdkClient = client(chain) + .with(blockfrost({ baseUrl: blockfrostUrls[resolvedKey], projectId: import.meta.env.VITE_BLOCKFROST_PROJECT_ID || "" })) + .with(cip30Wallet(api)) // Build transaction (convert ADA to lovelace: 1 ADA = 1,000,000 lovelace) const lovelaceAmount = BigInt(Math.floor(amountLovelace * 1_000_000)) @@ -88,7 +84,7 @@ export default function TransactionBuilder() { const assetsToSend = Assets.fromLovelace(lovelaceAmount) // Build, sign, and submit transaction - const tx = await client + const tx = await sdkClient .newTx() .payToAddress({ address: parsedAddress, diff --git a/packages/evolution-devnet/src/Cluster.ts b/packages/evolution-devnet/src/Cluster.ts index d49fa385..b94aaf26 100644 --- a/packages/evolution-devnet/src/Cluster.ts +++ b/packages/evolution-devnet/src/Cluster.ts @@ -519,17 +519,9 @@ export interface SlotConfig { * @example * ```typescript * import * as Cluster from "@evolution-sdk/devnet/Cluster" - * import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" * * const cluster = await Cluster.make({ ... }) * const slotConfig = Cluster.getSlotConfig(cluster) - * - * const client = createClient({ - * network: 0, - * slotConfig, - * provider: { type: "kupmios", kupoUrl: "...", ogmiosUrl: "..." }, - * wallet: { type: "seed", mnemonic: "..." } - * }) * ``` * * @since 2.0.0 @@ -547,3 +539,47 @@ export const getSlotConfig = (cluster: Cluster): SlotConfig => { slotLength } } + +/** + * Create a Chain descriptor for a running devnet cluster. + * + * The returned object is structurally compatible with the `Chain` interface + * from `@evolution-sdk/evolution` and can be passed directly to `client()`. + * + * @example + * ```typescript + * const cluster = await Cluster.make({ ... }) + * await Cluster.start(cluster) + * const c = client(Cluster.getChain(cluster)) + * .with(kupmios({ kupoUrl: "...", ogmiosUrl: "..." })) + * .with(seedWallet({ mnemonic: "..." })) + * ``` + * + * @since 2.1.0 + * @category utilities + */ +export const getChain = (cluster: Cluster) => ({ + name: "Devnet", + id: 0 as const, + networkMagic: cluster.shelleyGenesis.networkMagic, + slotConfig: getSlotConfig(cluster), + epochLength: cluster.shelleyGenesis.epochLength +}) + +/** + * A minimal testnet chain descriptor for bootstrapping (e.g., deriving wallet addresses + * before a cluster is started). Uses `id: 0` so addresses are in testnet bech32 format. + * + * Slot config values are zero-based placeholders — valid for address derivation but + * not for time-based validity windows. Use `getChain(cluster)` once the cluster is running. + * + * @since 2.1.0 + * @category constants + */ +export const BOOTSTRAP_CHAIN = { + name: "Devnet", + id: 0 as const, + networkMagic: Config.DEFAULT_DEVNET_CONFIG.networkMagic, + slotConfig: { zeroTime: 0n, zeroSlot: 0n, slotLength: 1000 }, + epochLength: Config.DEFAULT_DEVNET_CONFIG.shelleyGenesis.epochLength ?? 432000 +} as const diff --git a/packages/evolution-devnet/test/Client.Devnet.test.ts b/packages/evolution-devnet/test/Client.Devnet.test.ts index 39b1bd80..8c202d13 100644 --- a/packages/evolution-devnet/test/Client.Devnet.test.ts +++ b/packages/evolution-devnet/test/Client.Devnet.test.ts @@ -2,9 +2,8 @@ import { describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/Address" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import type { ProtocolParameters } from "@evolution-sdk/evolution/sdk/ProtocolParameters" import { afterAll, beforeAll } from "vitest" @@ -23,27 +22,12 @@ describe("Client with Devnet", () => { "test test test test test test test test test test test test test test test test test test test test test test test sauce" const createTestClient = () => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1443", - ogmiosUrl: "http://localhost:1338" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex: 0 - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1443", ogmiosUrl: "http://localhost:1338" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) beforeAll(async () => { - const testClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0 } - }) + const testClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) - const testAddress = await testClient.address() + const testAddress = await testClient.getAddress() const testAddressHex = CoreAddress.toHex(testAddress) genesisConfig = { @@ -92,7 +76,7 @@ describe("Client with Devnet", () => { it("should create signing client and query wallet address", { timeout: 30_000 }, async () => { const client = createTestClient() - const address = await client.address() + const address = await client.getAddress() expect(address).toBeDefined() const addressBech32 = CoreAddress.toBech32(address) expect(addressBech32).toMatch(/^addr_test/) @@ -125,7 +109,7 @@ describe("Client with Devnet", () => { } const client = createTestClient() - const genesisAddress = await client.address() + const genesisAddress = await client.getAddress() const genesisAddressBech32 = CoreAddress.toBech32(genesisAddress) const genesisUtxo = genesisUtxos.find((u) => CoreAddress.toBech32(u.address) === genesisAddressBech32) diff --git a/packages/evolution-devnet/test/Client.WatchUtxos.test.ts b/packages/evolution-devnet/test/Client.WatchUtxos.test.ts new file mode 100644 index 00000000..749937da --- /dev/null +++ b/packages/evolution-devnet/test/Client.WatchUtxos.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "@effect/vitest" +import * as Cluster from "@evolution-sdk/devnet/Cluster" +import * as Config from "@evolution-sdk/devnet/Config" +import * as Genesis from "@evolution-sdk/devnet/Genesis" +import { Cardano, client, kupmios, seedWallet } from "@evolution-sdk/evolution" +import * as CoreAddress from "@evolution-sdk/evolution/Address" +import { afterAll, beforeAll } from "vitest" + +const CoreAssets = Cardano.Assets + +/** + * Streaming capability integration test. + * + * Validates that `watchUtxos` (Stream → AsyncIterable) + * works end-to-end against a real Kupo instance on a local devnet. + */ +describe("Client.watchUtxos with Devnet", () => { + let devnetCluster: Cluster.Cluster | undefined + let genesisUtxos: ReadonlyArray = [] + let genesisConfig: Config.ShelleyGenesis + + const TEST_MNEMONIC = + "test test test test test test test test test test test test test test test test test test test test test test test sauce" + + const KUPO_PORT = 1455 + const OGMIOS_PORT = 1347 + + const createTestClient = () => + client(Cluster.getChain(devnetCluster!)) + .with(kupmios({ kupoUrl: `http://localhost:${KUPO_PORT}`, ogmiosUrl: `http://localhost:${OGMIOS_PORT}` })) + .with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) + + beforeAll(async () => { + const testClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) + + const testAddress = await testClient.getAddress() + const testAddressHex = CoreAddress.toHex(testAddress) + + genesisConfig = { + ...Config.DEFAULT_SHELLEY_GENESIS, + slotLength: 0.02, + epochLength: 50, + activeSlotsCoeff: 1.0, + initialFunds: { [testAddressHex]: 900_000_000_000 } + } + + devnetCluster = await Cluster.make({ + clusterName: "client-watch-utxos-test", + ports: { node: 6012, submit: 9012 }, + shelleyGenesis: genesisConfig, + kupo: { enabled: true, port: KUPO_PORT, logLevel: "Info" }, + ogmios: { enabled: true, port: OGMIOS_PORT, logLevel: "info" } + }) + + await Cluster.start(devnetCluster) + await new Promise((resolve) => setTimeout(resolve, 3_000)) + + genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig) + }, 180_000) + + afterAll(async () => { + if (devnetCluster) { + await Cluster.stop(devnetCluster) + await Cluster.remove(devnetCluster) + } + }, 60_000) + + it("should log the watchUtxos AsyncIterable and emit the received UTxO", { timeout: 60_000 }, async () => { + const testClient = createTestClient() + const address = await testClient.getAddress() + + const receiverAddress = CoreAddress.fromBech32( + "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae" + ) + + const genesisUtxo = genesisUtxos.find( + (utxo) => CoreAddress.toBech32(utxo.address) === CoreAddress.toBech32(address) + ) + expect(genesisUtxo).toBeDefined() + + const receivedUtxos: Array = [] + const asyncIter = testClient.watchUtxos(receiverAddress, 500) + // eslint-disable-next-line no-console + console.log("asyncIter:", asyncIter) + + const watchPromise = (async () => { + for await (const utxo of asyncIter) { + // eslint-disable-next-line no-console + console.log("received utxo:", utxo) + receivedUtxos.push(utxo) + break + } + })() + + await new Promise((resolve) => setTimeout(resolve, 200)) + + const txHash = await testClient + .newTx() + .payToAddress({ address: receiverAddress, assets: CoreAssets.fromLovelace(5_000_000n) }) + .build({ availableUtxos: [genesisUtxo!] }) + .then((signBuilder) => signBuilder.sign()) + .then((submitBuilder) => submitBuilder.submit()) + + expect(Cardano.TransactionHash.toHex(txHash).length).toBe(64) + expect(await testClient.awaitTx(txHash, 1000)).toBe(true) + + await watchPromise + + expect(receivedUtxos.length).toBe(1) + expect(receivedUtxos[0].assets.lovelace).toBe(5_000_000n) + expect(CoreAddress.toBech32(receivedUtxos[0].address)).toBe(CoreAddress.toBech32(receiverAddress)) + }) +}) diff --git a/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts b/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts index 60fcb3f0..b9b190b7 100644 --- a/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts @@ -9,10 +9,9 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" import * as KeyHash from "@evolution-sdk/evolution/KeyHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" describe("TxBuilder addSigner (Devnet Submit)", () => { @@ -25,31 +24,13 @@ describe("TxBuilder addSigner (Devnet Submit)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") - const slotConfig = Cluster.getSlotConfig(devnetCluster) - return createClient({ - network: 0, - slotConfig, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1449", - ogmiosUrl: "http://localhost:1344" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + return client(Cluster.getChain(devnetCluster)).with(kupmios({ kupoUrl: "http://localhost:1449", ogmiosUrl: "http://localhost:1344" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) } beforeAll(async () => { - const tempClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" } - }) + const tempClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })) - const testAddress = await tempClient.address() + const testAddress = await tempClient.getAddress() const testAddressHex = Address.toHex(testAddress) genesisConfig = { @@ -83,7 +64,7 @@ describe("TxBuilder addSigner (Devnet Submit)", () => { it("should include requiredSigners in transaction body and submit successfully", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Extract payment key hash from address credential const paymentCredential = myAddress.paymentCredential @@ -123,8 +104,8 @@ describe("TxBuilder addSigner (Devnet Submit)", () => { const client1 = createTestClient(0) const client2 = createTestClient(1) - const address1 = await client1.address() - const address2 = await client2.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() // Extract payment key hashes from both addresses const credential1 = address1.paymentCredential diff --git a/packages/evolution-devnet/test/TxBuilder.Chain.test.ts b/packages/evolution-devnet/test/TxBuilder.Chain.test.ts index f9ab95c8..1dc07ea8 100644 --- a/packages/evolution-devnet/test/TxBuilder.Chain.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Chain.test.ts @@ -2,10 +2,9 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" import type { SignBuilder } from "@evolution-sdk/evolution/sdk/builders/SignBuilder" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" describe("TxBuilder.chainResult", () => { @@ -18,31 +17,13 @@ describe("TxBuilder.chainResult", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") - const slotConfig = Cluster.getSlotConfig(devnetCluster) - return createClient({ - network: 0, - slotConfig, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1449", - ogmiosUrl: "http://localhost:1344" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + return client(Cluster.getChain(devnetCluster)).with(kupmios({ kupoUrl: "http://localhost:1449", ogmiosUrl: "http://localhost:1344" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) } beforeAll(async () => { - const tempClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" } - }) + const tempClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })) - const testAddress = await tempClient.address() + const testAddress = await tempClient.getAddress() const testAddressHex = Address.toHex(testAddress) genesisConfig = { @@ -76,7 +57,7 @@ describe("TxBuilder.chainResult", () => { it("should chain multiple transactions and submit them all", { timeout: 90_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() const TX_COUNT = 5 // Build chained transactions using build() + chainResult diff --git a/packages/evolution-devnet/test/TxBuilder.Compose.test.ts b/packages/evolution-devnet/test/TxBuilder.Compose.test.ts index 6d4c5a3c..81e93e3a 100644 --- a/packages/evolution-devnet/test/TxBuilder.Compose.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Compose.test.ts @@ -9,9 +9,8 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" // Alias for readability const Time = Cardano.Time @@ -26,31 +25,13 @@ describe("TxBuilder compose (Devnet Submit)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") - const slotConfig = Cluster.getSlotConfig(devnetCluster) - return createClient({ - network: 0, - slotConfig, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1451", - ogmiosUrl: "http://localhost:1346" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + return client(Cluster.getChain(devnetCluster)).with(kupmios({ kupoUrl: "http://localhost:1451", ogmiosUrl: "http://localhost:1346" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) } beforeAll(async () => { - const tempClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" } - }) + const tempClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })) - const testAddress = await tempClient.address() + const testAddress = await tempClient.getAddress() const testAddressHex = Address.toHex(testAddress) genesisConfig = { @@ -84,7 +65,7 @@ describe("TxBuilder compose (Devnet Submit)", () => { it("should compose payment with validity constraints", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Create a payment builder const paymentBuilder = client.newTx().payToAddress({ @@ -123,8 +104,8 @@ describe("TxBuilder compose (Devnet Submit)", () => { const client1 = createTestClient(0) const client2 = createTestClient(1) - const address1 = await client1.address() - const address2 = await client2.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() // Create separate payment builders for different addresses const payment1 = client1.newTx().payToAddress({ @@ -161,7 +142,7 @@ describe("TxBuilder compose (Devnet Submit)", () => { it("should compose builder with addSigner + metadata + payment", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Extract payment credential const paymentCredential = myAddress.paymentCredential @@ -208,7 +189,7 @@ describe("TxBuilder compose (Devnet Submit)", () => { it("should compose stake registration with payment and metadata", { timeout: 90_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Get stake credential from address if (!("stakingCredential" in myAddress) || !myAddress.stakingCredential) { @@ -257,7 +238,7 @@ describe("TxBuilder compose (Devnet Submit)", () => { it("should verify getPrograms returns accumulated operations", { timeout: 30_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Build a transaction with multiple operations const builder = client @@ -296,8 +277,8 @@ describe("TxBuilder compose (Devnet Submit)", () => { const client1 = createTestClient(0) const client2 = createTestClient(1) - const address1 = await client1.address() - const address2 = await client2.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() // Create builders from different clients const builder1 = client1.newTx().payToAddress({ diff --git a/packages/evolution-devnet/test/TxBuilder.Governance.test.ts b/packages/evolution-devnet/test/TxBuilder.Governance.test.ts index 917922b4..ff4caf26 100644 --- a/packages/evolution-devnet/test/TxBuilder.Governance.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Governance.test.ts @@ -7,13 +7,14 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" +import type { Cardano} from "@evolution-sdk/evolution"; +import { client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" import * as Anchor from "@evolution-sdk/evolution/Anchor" import * as Bytes from "@evolution-sdk/evolution/Bytes" import * as Bytes32 from "@evolution-sdk/evolution/Bytes32" import * as Credential from "@evolution-sdk/evolution/Credential" import * as KeyHash from "@evolution-sdk/evolution/KeyHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as Url from "@evolution-sdk/evolution/Url" describe("TxBuilder Governance Operations", () => { @@ -26,31 +27,15 @@ describe("TxBuilder Governance Operations", () => { "test test test test test test test test test test test test test test test test test test test test test test test sauce" const createTestClient = (accountIndex: number = 0) => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1452", - ogmiosUrl: "http://localhost:1342" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1452", ogmiosUrl: "http://localhost:1342" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) beforeAll(async () => { // Create clients for governance tests const accounts = [0, 1, 2, 3, 4].map((accountIndex) => - createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" } - }) + client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) ) - const addresses = await Promise.all(accounts.map((client) => client.address())) + const addresses = await Promise.all(accounts.map((client) => client.getAddress())) const addressHexes = addresses.map((addr) => Address.toHex(addr)) // Extract committee member key hashes from payment credentials @@ -117,7 +102,7 @@ describe("TxBuilder Governance Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const drepCredential = walletAddress.paymentCredential const anchor = new Anchor.Anchor({ @@ -142,7 +127,7 @@ describe("TxBuilder Governance Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const drepCredential = walletAddress.paymentCredential // Register DRep first @@ -184,7 +169,7 @@ describe("TxBuilder Governance Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const drepCredential = walletAddress.paymentCredential // Register DRep @@ -216,7 +201,7 @@ describe("TxBuilder Governance Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const coldCredential = walletAddress.paymentCredential const hotKeyHashBytes = KeyHash.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") @@ -239,7 +224,7 @@ describe("TxBuilder Governance Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const coldCredential = walletAddress.paymentCredential const anchor = new Anchor.Anchor({ diff --git a/packages/evolution-devnet/test/TxBuilder.Metadata.test.ts b/packages/evolution-devnet/test/TxBuilder.Metadata.test.ts index 8036a21f..ebc6b35f 100644 --- a/packages/evolution-devnet/test/TxBuilder.Metadata.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Metadata.test.ts @@ -9,9 +9,8 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" import { fromEntries } from "@evolution-sdk/evolution/TransactionMetadatum" @@ -25,31 +24,13 @@ describe("TxBuilder attachMetadata (Devnet Submit)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") - const slotConfig = Cluster.getSlotConfig(devnetCluster) - return createClient({ - network: 0, - slotConfig, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1450", - ogmiosUrl: "http://localhost:1345" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + return client(Cluster.getChain(devnetCluster)).with(kupmios({ kupoUrl: "http://localhost:1450", ogmiosUrl: "http://localhost:1345" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) } beforeAll(async () => { - const tempClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" } - }) + const tempClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })) - const testAddress = await tempClient.address() + const testAddress = await tempClient.getAddress() const testAddressHex = Address.toHex(testAddress) genesisConfig = { @@ -83,7 +64,7 @@ describe("TxBuilder attachMetadata (Devnet Submit)", () => { it("should attach simple text metadata (CIP-20 message) and submit successfully", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() const signBuilder = await client .newTx() @@ -123,7 +104,7 @@ describe("TxBuilder attachMetadata (Devnet Submit)", () => { it("should attach multiple metadata entries with different labels", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() const signBuilder = await client .newTx() @@ -176,7 +157,7 @@ describe("TxBuilder attachMetadata (Devnet Submit)", () => { it("should attach complex NFT-like metadata (CIP-25 style)", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // CIP-25 style NFT metadata const nftMetadata = fromEntries([ diff --git a/packages/evolution-devnet/test/TxBuilder.Mint.test.ts b/packages/evolution-devnet/test/TxBuilder.Mint.test.ts index 1e413da9..c387dd2b 100644 --- a/packages/evolution-devnet/test/TxBuilder.Mint.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Mint.test.ts @@ -2,13 +2,12 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/Address" import * as AssetName from "@evolution-sdk/evolution/AssetName" import * as NativeScripts from "@evolution-sdk/evolution/NativeScripts" import * as PolicyId from "@evolution-sdk/evolution/PolicyId" import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as Text from "@evolution-sdk/evolution/Text" import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" @@ -30,27 +29,12 @@ describe("TxBuilder Minting (Devnet Submit)", () => { const ASSET_NAME = "TestToken" const createTestClient = () => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1443", - ogmiosUrl: "http://localhost:1338" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex: 0 - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1443", ogmiosUrl: "http://localhost:1338" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) beforeAll(async () => { - const testClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0 } - }) + const testClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) - const testAddress = await testClient.address() + const testAddress = await testClient.getAddress() const testAddressHex = CoreAddress.toHex(testAddress) // Get payment key hash from client's address for native script @@ -103,7 +87,7 @@ describe("TxBuilder Minting (Devnet Submit)", () => { } const client = createTestClient() - const address = await client.address() + const address = await client.getAddress() // Use pre-calculated genesis UTxOs (Kupo may not have synced yet) const genesisUtxo = genesisUtxos.find((u) => CoreAddress.toBech32(u.address) === CoreAddress.toBech32(address)) @@ -170,7 +154,7 @@ describe("TxBuilder Minting (Devnet Submit)", () => { } const client = createTestClient() - const address = await client.address() + const address = await client.getAddress() const assetNameHex = Text.toHex(ASSET_NAME) const unit = policyId + assetNameHex diff --git a/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts b/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts index c4c43850..503837ef 100644 --- a/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts @@ -11,11 +11,10 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" import * as NativeScripts from "@evolution-sdk/evolution/NativeScripts" import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as Text from "@evolution-sdk/evolution/Text" import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" import * as UTxO from "@evolution-sdk/evolution/UTxO" @@ -42,31 +41,13 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") - const slotConfig = Cluster.getSlotConfig(devnetCluster) - return createClient({ - network: 0, - slotConfig, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1449", - ogmiosUrl: "http://localhost:1344" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + return client(Cluster.getChain(devnetCluster)).with(kupmios({ kupoUrl: "http://localhost:1449", ogmiosUrl: "http://localhost:1344" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) } beforeAll(async () => { - const tempClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" } - }) + const tempClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })) - const testAddress = await tempClient.address() + const testAddress = await tempClient.getAddress() const testAddressHex = Address.toHex(testAddress) genesisConfig = { @@ -102,8 +83,8 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const client1 = createTestClient(0) const client2 = createTestClient(1) - const address1 = await client1.address() - const address2 = await client2.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() const credential1 = address1.paymentCredential const credential2 = address2.paymentCredential @@ -163,8 +144,8 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const client1 = createTestClient(0) const client2 = createTestClient(1) - const address1 = await client1.address() - const address2 = await client2.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() const credential1 = address1.paymentCredential const credential2 = address2.paymentCredential @@ -217,9 +198,9 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const client2 = createTestClient(1) const client3 = createTestClient(2) - const address1 = await client1.address() - const address2 = await client2.address() - const address3 = await client3.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() + const address3 = await client3.getAddress() const credential1 = address1.paymentCredential const credential2 = address2.paymentCredential @@ -276,7 +257,7 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { if (!devnetCluster) throw new Error("Cluster not initialized") const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() const paymentCredential = myAddress.paymentCredential if (paymentCredential._tag !== "KeyHash") { @@ -334,7 +315,7 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { if (!devnetCluster) throw new Error("Cluster not initialized") const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() const paymentCredential = myAddress.paymentCredential if (paymentCredential._tag !== "KeyHash") { @@ -403,8 +384,8 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const client1 = createTestClient(0) const client2 = createTestClient(1) - const address1 = await client1.address() - const address2 = await client2.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() const credential1 = address1.paymentCredential const credential2 = address2.paymentCredential @@ -480,8 +461,8 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const client1 = createTestClient(0) const client2 = createTestClient(1) - const address1 = await client1.address() - const address2 = await client2.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() const credential1 = address1.paymentCredential const credential2 = address2.paymentCredential @@ -542,8 +523,6 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const mintTx = await mintSignBuilder.toTransaction() // Both signers still need to sign (native script requires signatures) - // When using client.signTx directly, reference UTxOs are auto-fetched - // so the wallet can determine required signers from reference scripts const mintWitness1 = await mintSignBuilder.partialSign() const mintWitness2 = await client2.signTx(mintTx) @@ -560,8 +539,8 @@ describe("TxBuilder NativeScript (Devnet Submit)", () => { const client1 = createTestClient(0) const client2 = createTestClient(1) - const address1 = await client1.address() - const address2 = await client2.address() + const address1 = await client1.getAddress() + const address2 = await client2.getAddress() const credential1 = address1.paymentCredential const credential2 = address2.paymentCredential diff --git a/packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts b/packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts index bc328d91..f8dab6b8 100644 --- a/packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.PlutusMint.test.ts @@ -10,7 +10,7 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/Address" import * as AssetName from "@evolution-sdk/evolution/AssetName" import * as Bytes from "@evolution-sdk/evolution/Bytes" @@ -18,7 +18,6 @@ import * as Data from "@evolution-sdk/evolution/Data" import * as PlutusV3 from "@evolution-sdk/evolution/PlutusV3" import * as PolicyId from "@evolution-sdk/evolution/PolicyId" import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as Text from "@evolution-sdk/evolution/Text" import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" @@ -65,30 +64,15 @@ describe("TxBuilder Plutus Minting (Devnet Submit)", () => { const calculatedPolicyId = ScriptHash.toHex(scriptHash) const createTestClient = () => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1444", - ogmiosUrl: "http://localhost:1339" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex: 0 - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1444", ogmiosUrl: "http://localhost:1339" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) beforeAll(async () => { // Verify our script hash calculation matches the blueprint expect(calculatedPolicyId).toBe(SIMPLE_MINT_POLICY_ID_HEX) - const testClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0 } - }) + const testClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) - const testAddress = await testClient.address() + const testAddress = await testClient.getAddress() const testAddressHex = CoreAddress.toHex(testAddress) genesisConfig = { @@ -131,7 +115,7 @@ describe("TxBuilder Plutus Minting (Devnet Submit)", () => { } const client = createTestClient() - const address = await client.address() + const address = await client.getAddress() // Use pre-calculated genesis UTxOs (Kupo may not have synced yet) const genesisUtxo = genesisUtxos.find((u) => CoreAddress.toBech32(u.address) === CoreAddress.toBech32(address)) @@ -212,7 +196,7 @@ describe("TxBuilder Plutus Minting (Devnet Submit)", () => { it("should mint then burn tokens with PlutusV3 simple_mint script", { timeout: 60_000 }, async () => { const client = createTestClient() - const address = await client.address() + const address = await client.getAddress() const assetNameHex = Text.toHex("BurnToken") const unit = SIMPLE_MINT_POLICY_ID_HEX + assetNameHex diff --git a/packages/evolution-devnet/test/TxBuilder.Pool.test.ts b/packages/evolution-devnet/test/TxBuilder.Pool.test.ts index 796b1313..7a3db470 100644 --- a/packages/evolution-devnet/test/TxBuilder.Pool.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Pool.test.ts @@ -7,6 +7,7 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" +import { client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" import * as Bytes32 from "@evolution-sdk/evolution/Bytes32" import type * as EpochNo from "@evolution-sdk/evolution/EpochNo" @@ -16,7 +17,6 @@ import * as PoolKeyHash from "@evolution-sdk/evolution/PoolKeyHash" import * as PoolMetadata from "@evolution-sdk/evolution/PoolMetadata" import * as PoolParams from "@evolution-sdk/evolution/PoolParams" import * as RewardAccount from "@evolution-sdk/evolution/RewardAccount" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as SingleHostAddr from "@evolution-sdk/evolution/SingleHostAddr" import * as UnitInterval from "@evolution-sdk/evolution/UnitInterval" import * as Url from "@evolution-sdk/evolution/Url" @@ -31,31 +31,15 @@ describe("TxBuilder Pool Operations", () => { "test test test test test test test test test test test test test test test test test test test test test test test sauce" const createTestClient = (accountIndex: number = 0) => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1453", - ogmiosUrl: "http://localhost:1343" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1453", ogmiosUrl: "http://localhost:1343" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) beforeAll(async () => { // Create clients for pool tests const accounts = [0, 1].map((accountIndex) => - createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" } - }) + client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) ) - const addresses = await Promise.all(accounts.map((client) => client.address())) + const addresses = await Promise.all(accounts.map((client) => client.getAddress())) const addressHexes = addresses.map((addr) => Address.toHex(addr)) genesisConfig = { @@ -108,7 +92,7 @@ describe("TxBuilder Pool Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const poolKeyHash = walletAddress.paymentCredential._tag === "KeyHash" @@ -170,7 +154,7 @@ describe("TxBuilder Pool Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const poolKeyHash = walletAddress.paymentCredential._tag === "KeyHash" diff --git a/packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts b/packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts index 62534689..f1a27c8c 100644 --- a/packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.RedeemerBuilder.test.ts @@ -9,7 +9,7 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/Address" import * as AssetName from "@evolution-sdk/evolution/AssetName" import * as Bytes from "@evolution-sdk/evolution/Bytes" @@ -18,7 +18,6 @@ import * as InlineDatum from "@evolution-sdk/evolution/InlineDatum" import * as PlutusV3 from "@evolution-sdk/evolution/PlutusV3" import * as PolicyId from "@evolution-sdk/evolution/PolicyId" import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as Text from "@evolution-sdk/evolution/Text" import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" import { Schema } from "effect" @@ -84,30 +83,15 @@ describe("TxBuilder RedeemerBuilder", () => { const calculatedPolicyId = ScriptHash.toHex(scriptHashValue) const createTestClient = () => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1445", - ogmiosUrl: "http://localhost:1340" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex: 0 - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1445", ogmiosUrl: "http://localhost:1340" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) beforeAll(async () => { // Verify our script hash calculation matches the blueprint expect(calculatedPolicyId).toBe(MINT_MULTI_POLICY_ID_HEX) - const testClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0 } - }) + const testClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) - const testAddress = await testClient.address() + const testAddress = await testClient.getAddress() const testAddressHex = CoreAddress.toHex(testAddress) genesisConfig = { @@ -146,7 +130,7 @@ describe("TxBuilder RedeemerBuilder", () => { } const client = createTestClient() - const walletAddress = await client.address() + const walletAddress = await client.getAddress() // Use pre-calculated genesis UTxOs const genesisUtxo = genesisUtxos.find( diff --git a/packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts b/packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts index 780d354d..af18c710 100644 --- a/packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.ScriptStake.test.ts @@ -17,14 +17,13 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/Address" import * as Bytes from "@evolution-sdk/evolution/Bytes" import * as Data from "@evolution-sdk/evolution/Data" import * as InlineDatum from "@evolution-sdk/evolution/InlineDatum" import * as PlutusV3 from "@evolution-sdk/evolution/PlutusV3" import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import plutusJson from "../../evolution/test/spec/plutus.json" @@ -82,31 +81,15 @@ describe("TxBuilder Script Stake Operations", () => { } const createTestClient = (accountIndex: number = 0) => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1447", - ogmiosUrl: "http://localhost:1342" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" // Need Base address to have stake credential for paying fees - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1447", ogmiosUrl: "http://localhost:1342" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) beforeAll(async () => { // Verify our script hash calculation matches the blueprint expect(calculatedScriptHash).toBe(STAKE_MULTI_SCRIPT_HASH) - const testClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" } - }) + const testClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })) - const testAddress = await testClient.address() + const testAddress = await testClient.getAddress() const testAddressHex = CoreAddress.toHex(testAddress) genesisConfig = { diff --git a/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts b/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts index 32f4d546..3e0369f8 100644 --- a/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import { createAikenEvaluator } from "@evolution-sdk/aiken-uplc" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, newTx, preview } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/Address" import * as Bytes from "@evolution-sdk/evolution/Bytes" import * as Data from "@evolution-sdk/evolution/Data" @@ -8,11 +8,10 @@ import * as InlineDatum from "@evolution-sdk/evolution/InlineDatum" import * as PlutusV2 from "@evolution-sdk/evolution/PlutusV2" import * as PlutusV3 from "@evolution-sdk/evolution/PlutusV3" import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" -import type { TxBuilderConfig } from "@evolution-sdk/evolution/sdk/builders/TransactionBuilder" -import { makeTxBuilder } from "@evolution-sdk/evolution/sdk/builders/TransactionBuilder" -import { KupmiosProvider } from "@evolution-sdk/evolution/sdk/provider/Kupmios" +import type { KupmiosCapabilities } from "@evolution-sdk/evolution/sdk/client/Capabilities" +import type { Client } from "@evolution-sdk/evolution/sdk/client/Client" import { createScalusEvaluator } from "@evolution-sdk/scalus-uplc" -import { Schema } from "effect" +import { pipe, Schema } from "effect" import plutusJson from "../../evolution/test/spec/plutus.json" import * as Cluster from "../src/Cluster.js" @@ -27,7 +26,7 @@ describe("TxBuilder Script Handling", () => { // ============================================================================ let devnetCluster: Cluster.Cluster | undefined - let kupmiosProvider: KupmiosProvider + let kupmiosClient: Client & KupmiosCapabilities beforeAll(async () => { try { @@ -57,11 +56,9 @@ describe("TxBuilder Script Handling", () => { // Ogmios serves both HTTP (for JSON-RPC) and WebSocket on the same port const ogmiosUrl = "http://localhost:1337" - // Create provider using local Ogmios - // Note: Kupo URL is required but not used in these tests (only Ogmios for evaluation) - kupmiosProvider = new KupmiosProvider( - "http://localhost:1442", // Kupo (not used) - ogmiosUrl // Ogmios for script evaluation via HTTP + kupmiosClient = pipe( + client(preview), + kupmios({ kupoUrl: "http://localhost:1442", ogmiosUrl }) ) // eslint-disable-next-line no-console @@ -106,12 +103,7 @@ describe("TxBuilder Script Handling", () => { const CHANGE_ADDRESS = TESTNET_ADDRESSES[0] const RECEIVER_ADDRESS = TESTNET_ADDRESSES[1] - // baseConfig will use kupmiosProvider which is set in beforeAll - const baseConfig: TxBuilderConfig = { - get provider() { - return kupmiosProvider - } - } + // kupmiosClient is initialized in beforeAll // Simple PlutusV2 always-succeeds script (CBOR-wrapped) const ALWAYS_SUCCEED_SCRIPT_CBOR = "49480100002221200101" @@ -168,7 +160,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -233,7 +225,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -299,7 +291,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -378,7 +370,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -473,7 +465,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -532,7 +524,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -591,7 +583,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -661,7 +653,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -744,7 +736,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -813,7 +805,7 @@ describe("TxBuilder Script Handling", () => { lovelace: 10_000_000n }) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .readFrom({ referenceInputs: [refScriptUtxo] }) .collectFrom({ inputs: [spendUtxo] }) .payToAddress({ @@ -861,7 +853,7 @@ describe("TxBuilder Script Handling", () => { lovelace: 10_000_000n }) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .readFrom({ referenceInputs: [refScriptUtxo] }) .collectFrom({ inputs: [spendUtxo] }) .payToAddress({ @@ -907,7 +899,7 @@ describe("TxBuilder Script Handling", () => { lovelace: 10_000_000n }) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .readFrom({ referenceInputs: [refScriptUtxo] }) .collectFrom({ inputs: [spendUtxo] }) .payToAddress({ @@ -954,7 +946,7 @@ describe("TxBuilder Script Handling", () => { lovelace: 10_000_000n }) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .readFrom({ referenceInputs: [refScriptUtxo] }) .collectFrom({ inputs: [spendUtxo] }) .payToAddress({ @@ -998,7 +990,7 @@ describe("TxBuilder Script Handling", () => { lovelace: 10_000_000n }) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .readFrom({ referenceInputs: [refScriptUtxo1, refScriptUtxo2] }) .collectFrom({ inputs: [spendUtxo] }) .payToAddress({ @@ -1041,7 +1033,7 @@ describe("TxBuilder Script Handling", () => { lovelace: 10_000_000n }) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .readFrom({ referenceInputs: [refUtxo] }) .collectFrom({ inputs: [spendUtxo] }) .payToAddress({ @@ -1116,7 +1108,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -1189,7 +1181,7 @@ describe("TxBuilder Script Handling", () => { // Create redeemer const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -1248,7 +1240,7 @@ describe("TxBuilder Script Handling", () => { const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) - const builder = makeTxBuilder(baseConfig) + const builder = newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: redeemerData @@ -1295,7 +1287,7 @@ describe("TxBuilder Script Handling", () => { // 100-byte payload — exceeds the 64-byte bounded_bytes chunk size const largePayload = new Uint8Array(100).fill(0xaa) - const signBuilder = await makeTxBuilder(baseConfig) + const signBuilder = await newTx(kupmiosClient) .collectFrom({ inputs: [scriptUtxo], redeemer: Data.constr(0n, [largePayload]) }) .attachScript({ script: alwaysSucceedV3 }) .payToAddress({ diff --git a/packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts b/packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts index 8c40cff4..31589de2 100644 --- a/packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.SpendScriptRef.test.ts @@ -7,14 +7,13 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/Address" import * as Bytes from "@evolution-sdk/evolution/Bytes" import * as Data from "@evolution-sdk/evolution/Data" import * as InlineDatum from "@evolution-sdk/evolution/InlineDatum" import * as PlutusV3 from "@evolution-sdk/evolution/PlutusV3" import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" const CoreAssets = Cardano.Assets @@ -38,25 +37,14 @@ describe("TxBuilder Spend ScriptRef (Devnet Submit)", () => { CoreAddress.Address.make({ networkId: 0, paymentCredential: alwaysSucceedScriptHash }) const createTestClient = () => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1454", - ogmiosUrl: "http://localhost:1346" - }, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0 } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1454", ogmiosUrl: "http://localhost:1346" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) beforeAll(async () => { expect(ScriptHash.toHex(alwaysSucceedScriptHash)).toBe(ALWAYS_SUCCEED_HASH) - const testClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0 } - }) + const testClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0 })) - const testAddress = await testClient.address() + const testAddress = await testClient.getAddress() const testAddressHex = CoreAddress.toHex(testAddress) genesisConfig = { @@ -95,7 +83,7 @@ describe("TxBuilder Spend ScriptRef (Devnet Submit)", () => { if (genesisUtxos.length === 0) throw new Error("Genesis UTxOs not calculated") const client = createTestClient() - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const scriptAddress = makeScriptAddress() const genesisUtxo = genesisUtxos.find( diff --git a/packages/evolution-devnet/test/TxBuilder.Stake.test.ts b/packages/evolution-devnet/test/TxBuilder.Stake.test.ts index 8679400d..655f1e80 100644 --- a/packages/evolution-devnet/test/TxBuilder.Stake.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Stake.test.ts @@ -15,10 +15,10 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" +import { client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" import * as DRep from "@evolution-sdk/evolution/DRep" import * as PoolKeyHash from "@evolution-sdk/evolution/PoolKeyHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" // Default devnet stake pool ID from Config.ts const DEVNET_POOL_ID = "8a219b698d3b6e034391ae84cee62f1d76b6fbc45ddfe4e31e0d4b60" @@ -34,31 +34,15 @@ describe("TxBuilder Stake Operations", () => { // Create client for a specific account index (each test uses different account) const createTestClient = (accountIndex: number = 0) => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1446", - ogmiosUrl: "http://localhost:1341" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" // Need Base address to have stake credential - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1446", ogmiosUrl: "http://localhost:1341" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) beforeAll(async () => { // Create clients for each account we'll use in tests const accounts = [0, 1, 2, 3, 4, 5, 6, 7, 8].map((accountIndex) => - createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" } - }) + client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) ) - const addresses = await Promise.all(accounts.map((client) => client.address())) + const addresses = await Promise.all(accounts.map((client) => client.getAddress())) const addressHexes = addresses.map((addr) => Address.toHex(addr)) // Fund each account independently so tests don't share UTxOs @@ -115,7 +99,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() // Extract stake credential from wallet address // The wallet address should be a base address with a stake component @@ -180,7 +164,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const addressStruct = walletAddress if (!("stakingCredential" in addressStruct) || !addressStruct.stakingCredential) { @@ -230,7 +214,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const addressStruct = walletAddress if (!("stakingCredential" in addressStruct) || !addressStruct.stakingCredential) { @@ -280,7 +264,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const addressStruct = walletAddress if (!("stakingCredential" in addressStruct) || !addressStruct.stakingCredential) { @@ -319,7 +303,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const addressStruct = walletAddress if (!("stakingCredential" in addressStruct) || !addressStruct.stakingCredential) { @@ -361,7 +345,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const addressStruct = walletAddress if (!("stakingCredential" in addressStruct) || !addressStruct.stakingCredential) { @@ -406,7 +390,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const addressStruct = walletAddress if (!("stakingCredential" in addressStruct) || !addressStruct.stakingCredential) { @@ -456,7 +440,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const addressStruct = walletAddress if (!("stakingCredential" in addressStruct) || !addressStruct.stakingCredential) { @@ -506,7 +490,7 @@ describe("TxBuilder Stake Operations", () => { } const client = createTestClient(ACCOUNT_INDEX) - const walletAddress = await client.address() + const walletAddress = await client.getAddress() const addressStruct = walletAddress if (!("stakingCredential" in addressStruct) || !addressStruct.stakingCredential) { diff --git a/packages/evolution-devnet/test/TxBuilder.Validity.test.ts b/packages/evolution-devnet/test/TxBuilder.Validity.test.ts index b4bec974..1f69dfbd 100644 --- a/packages/evolution-devnet/test/TxBuilder.Validity.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Validity.test.ts @@ -15,9 +15,8 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" // Alias for readability const Time = Cardano.Time @@ -33,32 +32,14 @@ describe("TxBuilder Validity Interval", () => { // Creates a client with correct slot config for devnet const createTestClient = (accountIndex: number = 0) => { if (!devnetCluster) throw new Error("Cluster not initialized") - const slotConfig = Cluster.getSlotConfig(devnetCluster) - return createClient({ - network: 0, - slotConfig, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1448", - ogmiosUrl: "http://localhost:1343" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + return client(Cluster.getChain(devnetCluster)).with(kupmios({ kupoUrl: "http://localhost:1448", ogmiosUrl: "http://localhost:1343" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) } beforeAll(async () => { // Create a minimal client just to get the address (before cluster is ready) - const tempClient = createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" } - }) + const tempClient = client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" })) - const testAddress = await tempClient.address() + const testAddress = await tempClient.getAddress() const testAddressHex = Address.toHex(testAddress) genesisConfig = { @@ -92,7 +73,7 @@ describe("TxBuilder Validity Interval", () => { it("should build and submit transaction with TTL", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Set TTL to 5 minutes from now const ttl = Time.now() + 300_000n @@ -125,7 +106,7 @@ describe("TxBuilder Validity Interval", () => { it("should build and submit transaction with both validity bounds", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Valid from now until 5 minutes from now const from = Time.now() @@ -165,7 +146,7 @@ describe("TxBuilder Validity Interval", () => { it("should reject expired transaction", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Set TTL to 1 second ago (already expired) const expiredTtl = Time.now() - 1_000n @@ -187,7 +168,7 @@ describe("TxBuilder Validity Interval", () => { it("should reject transaction before validity start", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const myAddress = await client.address() + const myAddress = await client.getAddress() // Valid starting 5 minutes from now (not valid yet) const from = Time.now() + 300_000n diff --git a/packages/evolution-devnet/test/TxBuilder.Vote.test.ts b/packages/evolution-devnet/test/TxBuilder.Vote.test.ts index 2fdef359..3d2fe227 100644 --- a/packages/evolution-devnet/test/TxBuilder.Vote.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Vote.test.ts @@ -7,6 +7,7 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" +import { client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" import * as Anchor from "@evolution-sdk/evolution/Anchor" import * as Bytes32 from "@evolution-sdk/evolution/Bytes32" @@ -17,7 +18,6 @@ import * as KeyHash from "@evolution-sdk/evolution/KeyHash" import * as ProtocolParamUpdate from "@evolution-sdk/evolution/ProtocolParamUpdate" import * as ProtocolVersion from "@evolution-sdk/evolution/ProtocolVersion" import * as RewardAccount from "@evolution-sdk/evolution/RewardAccount" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as UnitInterval from "@evolution-sdk/evolution/UnitInterval" import * as Url from "@evolution-sdk/evolution/Url" import * as VotingProcedures from "@evolution-sdk/evolution/VotingProcedures" @@ -32,31 +32,15 @@ describe("TxBuilder Vote Operations (script-free)", () => { "test test test test test test test test test test test test test test test test test test test test test test test sauce" const createTestClient = (accountIndex: number = 0) => - createClient({ - network: 0, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1453", - ogmiosUrl: "http://localhost:1343" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + client(Cluster.getChain(devnetCluster!)).with(kupmios({ kupoUrl: "http://localhost:1453", ogmiosUrl: "http://localhost:1343" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) beforeAll(async () => { - // Create clients for multiple test accounts + // Create clients for vote tests const accounts = [0, 1, 2, 3].map((accountIndex) => - createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" } - }) + client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) ) - const addresses = await Promise.all(accounts.map((client) => client.address())) + const addresses = await Promise.all(accounts.map((client) => client.getAddress())) const addressHexes = addresses.map((addr) => Address.toHex(addr)) genesisConfig = { @@ -122,7 +106,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { const proposerClient = createTestClient(PROPOSER_ACCOUNT) - const proposerAddress = await proposerClient.address() + const proposerAddress = await proposerClient.getAddress() const proposerCredential = proposerAddress.paymentCredential // Step 1: Register key-based DRep (proposer) @@ -225,7 +209,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { it("creates InfoAction proposal (type 6)", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() if (!address.stakingCredential) { throw new Error("Address must have staking credential") @@ -259,7 +243,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { // Use account 0 as voter for this simple test const VOTER_ACCOUNT = 0 const voterClient = createTestClient(VOTER_ACCOUNT) - const voterAddress = await voterClient.address() + const voterAddress = await voterClient.getAddress() const voterUtxo = genesisUtxosByAccount.get(VOTER_ACCOUNT) if (!voterUtxo) throw new Error("Voter genesis UTxO not found for InfoAction vote") @@ -301,7 +285,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { it("creates NoConfidenceAction proposal (type 3)", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() if (!address.stakingCredential) { throw new Error("Address must have staking credential") @@ -337,7 +321,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { // Use account 1 as voter for NoConfidenceAction const VOTER_ACCOUNT = 1 const voterClient = createTestClient(VOTER_ACCOUNT) - const voterAddress = await voterClient.address() + const voterAddress = await voterClient.getAddress() const voterUtxo = genesisUtxosByAccount.get(VOTER_ACCOUNT) if (!voterUtxo) throw new Error("Voter genesis UTxO not found for NoConfidenceAction vote") @@ -382,7 +366,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { it("creates HardForkInitiationAction proposal (type 1)", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() if (!address.stakingCredential) { throw new Error("Address must have staking credential") @@ -419,7 +403,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { // Use account 2 as voter for HardForkInitiationAction const VOTER_ACCOUNT = 2 const voterClient = createTestClient(VOTER_ACCOUNT) - const voterAddress = await voterClient.address() + const voterAddress = await voterClient.getAddress() const voterUtxo = genesisUtxosByAccount.get(VOTER_ACCOUNT) if (!voterUtxo) throw new Error("Voter genesis UTxO not found for HardForkInitiationAction vote") @@ -464,7 +448,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { it("creates TreasuryWithdrawalsAction proposal (type 2)", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() if (!address.stakingCredential) { throw new Error("Address must have staking credential") @@ -504,7 +488,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { // Use account 3 as voter for TreasuryWithdrawalsAction const VOTER_ACCOUNT = 3 const voterClient = createTestClient(VOTER_ACCOUNT) - const voterAddress = await voterClient.address() + const voterAddress = await voterClient.getAddress() const voterUtxo = genesisUtxosByAccount.get(VOTER_ACCOUNT) if (!voterUtxo) throw new Error("Voter genesis UTxO not found for TreasuryWithdrawalsAction vote") @@ -549,7 +533,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { it("creates UpdateCommitteeAction proposal (type 4)", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() if (!address.stakingCredential) { throw new Error("Address must have staking credential") @@ -588,7 +572,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { // Use account 0 as voter for UpdateCommitteeAction const VOTER_ACCOUNT = 0 const voterClient = createTestClient(VOTER_ACCOUNT) - const voterAddress = await voterClient.address() + const voterAddress = await voterClient.getAddress() const voterUtxo = genesisUtxosByAccount.get(VOTER_ACCOUNT) if (!voterUtxo) throw new Error("Voter genesis UTxO not found for UpdateCommitteeAction vote") @@ -633,7 +617,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { it("creates NewConstitutionAction proposal (type 5)", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() if (!address.stakingCredential) { throw new Error("Address must have staking credential") @@ -679,7 +663,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { // Use account 1 as voter for NewConstitutionAction const VOTER_ACCOUNT = 1 const voterClient = createTestClient(VOTER_ACCOUNT) - const voterAddress = await voterClient.address() + const voterAddress = await voterClient.getAddress() const voterUtxo = genesisUtxosByAccount.get(VOTER_ACCOUNT) if (!voterUtxo) throw new Error("Voter genesis UTxO not found for NewConstitutionAction vote") @@ -724,7 +708,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { it("creates ParameterChangeAction proposal (type 0)", { timeout: 60_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() if (!address.stakingCredential) { throw new Error("Address must have staking credential") @@ -766,7 +750,7 @@ describe("TxBuilder Vote Operations (script-free)", () => { // Ensure a key-based DRep exists for account 1 (voter) const VOTER_ACCOUNT = 1 const voterClient = createTestClient(VOTER_ACCOUNT) - const voterAddress = await voterClient.address() + const voterAddress = await voterClient.getAddress() const voterUtxo = genesisUtxosByAccount.get(VOTER_ACCOUNT) if (!voterUtxo) throw new Error("Voter genesis UTxO not found") diff --git a/packages/evolution-devnet/test/TxBuilder.VoteValidators.test.ts b/packages/evolution-devnet/test/TxBuilder.VoteValidators.test.ts index b592b39e..6ba92e42 100644 --- a/packages/evolution-devnet/test/TxBuilder.VoteValidators.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.VoteValidators.test.ts @@ -10,7 +10,7 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" import * as Cluster from "@evolution-sdk/devnet/Cluster" import * as Config from "@evolution-sdk/devnet/Config" import * as Genesis from "@evolution-sdk/devnet/Genesis" -import { Cardano } from "@evolution-sdk/evolution" +import { Cardano , client, kupmios, seedWallet } from "@evolution-sdk/evolution" import * as Address from "@evolution-sdk/evolution/Address" import * as Anchor from "@evolution-sdk/evolution/Anchor" import * as Bytes from "@evolution-sdk/evolution/Bytes" @@ -22,7 +22,6 @@ import * as InlineDatum from "@evolution-sdk/evolution/InlineDatum" import * as PlutusV3 from "@evolution-sdk/evolution/PlutusV3" import * as RewardAccount from "@evolution-sdk/evolution/RewardAccount" import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash" -import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" import * as TransactionHash from "@evolution-sdk/evolution/TransactionHash" import * as Url from "@evolution-sdk/evolution/Url" import * as VotingProcedures from "@evolution-sdk/evolution/VotingProcedures" @@ -49,36 +48,18 @@ const makeAnchor = (url: string) => describe("TxBuilder Vote Validator (script DRep)", () => { let devnetCluster: Cluster.Cluster | undefined - let slotConfig: Cluster.SlotConfig | undefined const createTestClient = (accountIndex: number = 0) => { - if (!slotConfig) throw new Error("slotConfig not initialized") - return createClient({ - network: 0, - slotConfig, - provider: { - type: "kupmios", - kupoUrl: "http://localhost:1453", - ogmiosUrl: "http://localhost:1343" - }, - wallet: { - type: "seed", - mnemonic: TEST_MNEMONIC, - accountIndex, - addressType: "Base" - } - }) + if (!devnetCluster) throw new Error("Cluster not initialized") + return client(Cluster.getChain(devnetCluster)).with(kupmios({ kupoUrl: "http://localhost:1453", ogmiosUrl: "http://localhost:1343" })).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) } const genesisUtxosByAccount: Map = new Map() beforeAll(async () => { const accounts = [0, 1].map((accountIndex) => - createClient({ - network: 0, - wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" } - }) + client(Cluster.BOOTSTRAP_CHAIN).with(seedWallet({ mnemonic: TEST_MNEMONIC, accountIndex, addressType: "Base" })) ) - const addresses = await Promise.all(accounts.map((client) => client.address())) + const addresses = await Promise.all(accounts.map((client) => client.getAddress())) const genesisConfig: Config.ShelleyGenesis = { ...Config.DEFAULT_SHELLEY_GENESIS, @@ -103,8 +84,6 @@ describe("TxBuilder Vote Validator (script DRep)", () => { ogmios: { enabled: true, port: 1343, logLevel: "info" } }) - slotConfig = Cluster.getSlotConfig(devnetCluster) - await Cluster.start(devnetCluster) await new Promise((r) => setTimeout(r, 3_000)) }, 180_000) @@ -118,7 +97,7 @@ describe("TxBuilder Vote Validator (script DRep)", () => { it("validates always_yes_drep for Publishing and Voting", { timeout: 180_000 }, async () => { const client = createTestClient(0) - const address = await client.address() + const address = await client.getAddress() if (!address.stakingCredential) throw new Error("Need staking credential") // Register stake (required for proposal submission) @@ -201,8 +180,8 @@ describe("TxBuilder Vote Validator (script DRep)", () => { const client0 = createTestClient(0) const client1 = createTestClient(1) - const address0 = await client0.address() - const address1 = await client1.address() + const address0 = await client0.getAddress() + const address1 = await client1.getAddress() if (!address0.stakingCredential) throw new Error("Need staking credential") const pkh0 = address0.paymentCredential @@ -344,8 +323,8 @@ describe("TxBuilder Vote Validator (script DRep)", () => { const client0 = createTestClient(0) const client1 = createTestClient(1) - const address0 = await client0.address() - const address1 = await client1.address() + const address0 = await client0.getAddress() + const address1 = await client1.getAddress() if (!address0.stakingCredential) throw new Error("Need staking credential") const pkh0 = address0.paymentCredential diff --git a/packages/evolution/src/Address.ts b/packages/evolution/src/Address.ts index cf88a6a4..b5b0e111 100644 --- a/packages/evolution/src/Address.ts +++ b/packages/evolution/src/Address.ts @@ -338,3 +338,12 @@ export const getStakingCredential = (address: string): Credential.Credential | u const details = getAddressDetails(address) return details?.stakingCredential } + +/** + * Check if an address has a script payment credential (i.e. is a script address). + * + * @since 2.0.0 + * @category Utils + */ +export const isScriptAddress = (address: Address): boolean => + address.paymentCredential?._tag === "ScriptHash" diff --git a/packages/evolution/src/NativeScriptsOLD.ts b/packages/evolution/src/NativeScriptsOLD.ts deleted file mode 100644 index 807122dd..00000000 --- a/packages/evolution/src/NativeScriptsOLD.ts +++ /dev/null @@ -1,510 +0,0 @@ -import { Data, Either as E, FastCheck, ParseResult, Schema } from "effect" -import type { ParseIssue } from "effect/ParseResult" - -import * as CBOR from "./CBOR.js" -import * as Function from "./Function.js" - -/** - * Error class for Native script related operations. - * - * @since 2.0.0 - * @category errors - */ -export class NativeError extends Data.TaggedError("NativeError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Type representing a native script following cardano-cli JSON syntax. - * - * @since 2.0.0 - * @category model - */ -export type Native = - | { - type: "sig" - keyHash: string - } - | { - type: "before" - slot: number - } - | { - type: "after" - slot: number - } - | { - type: "all" - scripts: ReadonlyArray - } - | { - type: "any" - scripts: ReadonlyArray - } - | { - type: "atLeast" - required: number - scripts: ReadonlyArray - } - -/** - * Represents a cardano-cli JSON script syntax - * - * Native type follows the standard described in the - * link https://github.com/IntersectMBO/cardano-node/blob/1.26.1-with-cardano-cli/doc/reference/simple-scripts.md#json-script-syntax JSON script syntax documentation. - * - * @since 2.0.0 - * @category schemas - */ -export const NativeSchema: Schema.Schema = Schema.Union( - Schema.Struct({ - type: Schema.Literal("sig"), - keyHash: Schema.String - }), - Schema.Struct({ - type: Schema.Literal("before"), - slot: Schema.Number - }), - Schema.Struct({ - type: Schema.Literal("after"), - slot: Schema.Number - }), - Schema.Struct({ - type: Schema.Literal("all"), - scripts: Schema.Array(Schema.suspend((): Schema.Schema => NativeSchema)) - }), - Schema.Struct({ - type: Schema.Literal("any"), - scripts: Schema.Array(Schema.suspend((): Schema.Schema => NativeSchema)) - }), - Schema.Struct({ - type: Schema.Literal("atLeast"), - required: Schema.Number, - scripts: Schema.Array(Schema.suspend((): Schema.Schema => NativeSchema)) - }) -).annotations({ - identifier: "Native", - title: "Native Script", - description: "A native script following cardano-cli JSON syntax" -}) - -export const Native = NativeSchema - -/** - * Smart constructor for Native that validates and applies branding. - * - * @since 2.0.0 - * @category constructors - */ -export const make = (native: Native): Native => native - -/** - * CDDL schemas for native scripts. - * - * These schemas define the CBOR encoding format for native scripts according to the CDDL specification: - * - * - script_pubkey = (0, addr_keyhash) - * - script_all = (1, [* native_script]) - * - script_any = (2, [* native_script]) - * - script_n_of_k = (3, n : int64, [* native_script]) - * - invalid_before = (4, slot_no) - * - invalid_hereafter = (5, slot_no) - * - slot_no = uint .size 8 - * - * @since 2.0.0 - * @category schemas - */ - -const ScriptPubKeyCDDL = Schema.Tuple(Schema.Literal(0n), Schema.Uint8ArrayFromSelf) - -const ScriptAllCDDL = Schema.Tuple( - Schema.Literal(1n), - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(FromCDDL))) -) - -const ScriptAnyCDDL = Schema.Tuple( - Schema.Literal(2n), - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(FromCDDL))) -) - -const ScriptNOfKCDDL = Schema.Tuple( - Schema.Literal(3n), - CBOR.Integer, - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(FromCDDL))) -) - -const InvalidBeforeCDDL = Schema.Tuple(Schema.Literal(4n), CBOR.Integer) - -const InvalidHereafterCDDL = Schema.Tuple(Schema.Literal(5n), CBOR.Integer) - -/** - * CDDL representation of a native script as a union of tuple types. - * - * This type represents the low-level CBOR structure of native scripts, - * where each variant is encoded as a tagged tuple. - * - * @since 2.0.0 - * @category model - */ -export type NativeCDDL = - | readonly [0n, Uint8Array] - | readonly [1n, ReadonlyArray] - | readonly [2n, ReadonlyArray] - | readonly [3n, bigint, ReadonlyArray] - | readonly [4n, bigint] - | readonly [5n, bigint] - -export const CDDLSchema = Schema.Union( - ScriptPubKeyCDDL, - ScriptAllCDDL, - ScriptAnyCDDL, - ScriptNOfKCDDL, - InvalidBeforeCDDL, - InvalidHereafterCDDL -) - -/** - * Schema for NativeCDDL union type. - * - * @since 2.0.0 - * @category schemas - */ -export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Native), { - strict: true, - encode: (native) => internalEncodeCDDL(native), - decode: (cborTuple) => internalDecodeCDDL(cborTuple) -}) - -/** - * Convert a Native to its CDDL representation. - * - * @since 2.0.0 - * @category encoding - */ -export const internalEncodeCDDL = (native: Native): E.Either => - E.gen(function* () { - switch (native.type) { - case "sig": { - // Convert hex string keyHash to bytes for CBOR encoding - const keyHashBytes = yield* ParseResult.decodeEither(Schema.Uint8ArrayFromHex)(native.keyHash) - return [0n, keyHashBytes] as const - } - case "all": { - const scriptResults: Array = [] - for (const script of native.scripts) { - const encoded = yield* internalEncodeCDDL(script) - scriptResults.push(encoded) - } - return [1n, scriptResults] as const - } - case "any": { - const scriptResults: Array = [] - for (const script of native.scripts) { - const encoded = yield* internalEncodeCDDL(script) - scriptResults.push(encoded) - } - return [2n, scriptResults] as const - } - case "atLeast": { - const scriptResults: Array = [] - for (const script of native.scripts) { - const encoded = yield* internalEncodeCDDL(script) - scriptResults.push(encoded) - } - return [3n, BigInt(native.required), scriptResults] as const - } - case "before": { - return [4n, BigInt(native.slot)] as const - } - case "after": { - return [5n, BigInt(native.slot)] as const - } - } - }) - -/** - * Convert a CDDL representation back to a Native. - * - * This function recursively decodes nested CBOR scripts and constructs - * the appropriate Native instances. - * - * @since 2.0.0 - * @category decoding - */ -export const internalDecodeCDDL = (cborTuple: NativeCDDL): E.Either => - E.gen(function* () { - switch (cborTuple[0]) { - case 0n: { - // sig: [0, keyHash_bytes] - convert bytes back to hex string - const [, keyHashBytes] = cborTuple - const keyHash = yield* ParseResult.encodeEither(Schema.Uint8ArrayFromHex)(keyHashBytes) - return { - type: "sig" as const, - keyHash - } - } - case 1n: { - // all: [1, [native_script, ...]] - const [, scriptCBORs] = cborTuple - const scripts: Array = [] - for (const scriptCBOR of scriptCBORs) { - const script = yield* internalDecodeCDDL(scriptCBOR) - scripts.push(script) - } - return { - type: "all" as const, - scripts - } - } - case 2n: { - // any: [2, [native_script, ...]] - const [, scriptCBORs] = cborTuple - const scripts: Array = [] - for (const scriptCBOR of scriptCBORs) { - const script = yield* internalDecodeCDDL(scriptCBOR) - scripts.push(script) - } - return { - type: "any" as const, - scripts - } - } - case 3n: { - // atLeast: [3, required, [native_script, ...]] - const [, required, scriptCBORs] = cborTuple - const scripts: Array = [] - for (const scriptCBOR of scriptCBORs) { - const script = yield* internalDecodeCDDL(scriptCBOR) - scripts.push(script) - } - return { - type: "atLeast" as const, - required: Number(required), - scripts - } - } - case 4n: { - // before: [4, slot] - const [, slot] = cborTuple - return { - type: "before" as const, - slot: Number(slot) - } - } - case 5n: { - // after: [5, slot] - const [, slot] = cborTuple - return { - type: "after" as const, - slot: Number(slot) - } - } - default: - // This should never happen with proper CBOR validation - return yield* E.left(new ParseResult.Type(Schema.Literal(0, 1, 2, 3, 4, 5).ast, cborTuple[0])) - } - }) - -/** - * FastCheck arbitrary for Native scripts. - * Generates valid native scripts with bounded depth and sizes. - * - * Depth limit prevents exponential blow-up. At depth 0, only base cases are generated. - */ -const nativeArbitrary = (depth: number): FastCheck.Arbitrary => { - const baseSig = FastCheck.record({ - type: FastCheck.constant("sig" as const), - // 28-byte keyhash (56 hex chars) - keyHash: FastCheck.hexaString({ minLength: 56, maxLength: 56 }) - }) - - const baseBefore = FastCheck.record({ - type: FastCheck.constant("before" as const), - slot: FastCheck.integer({ min: 0, max: 10_000_000 }) - }) - - const baseAfter = FastCheck.record({ - type: FastCheck.constant("after" as const), - slot: FastCheck.integer({ min: 0, max: 10_000_000 }) - }) - - if (depth <= 0) { - return FastCheck.oneof(baseSig, baseBefore, baseAfter) - } - - const sub = nativeArbitrary(depth - 1) - const scriptsArray = FastCheck.array(sub, { minLength: 0, maxLength: 3 }) - - const all = scriptsArray.map((scripts) => ({ type: "all" as const, scripts })) - const any = scriptsArray.map((scripts) => ({ type: "any" as const, scripts })) - - const atLeast = FastCheck.array(sub, { minLength: 0, maxLength: 4 }).chain((scripts) => - FastCheck.integer({ min: 0, max: scripts.length }).map((required) => ({ - type: "atLeast" as const, - required, - scripts - })) - ) - - // Weight base cases a bit higher for performance and balance - return FastCheck.oneof( - { arbitrary: baseSig, weight: 3 }, - { arbitrary: baseBefore, weight: 2 }, - { arbitrary: baseAfter, weight: 2 }, - { arbitrary: all, weight: 1 }, - { arbitrary: any, weight: 1 }, - { arbitrary: atLeast, weight: 1 } - ) -} - -export const arbitrary: FastCheck.Arbitrary = nativeArbitrary(2) - -/** - * Deep structural equality for Native scripts. - * Compares shape, values and recurses into nested scripts. - */ -export const equals = (a: Native, b: Native): boolean => { - if (a.type !== b.type) return false - switch (a.type) { - case "sig": - return a.keyHash === (b as any).keyHash - case "before": - return a.slot === (b as any).slot - case "after": - return a.slot === (b as any).slot - case "all": - case "any": { - const as = a.scripts - const bs = (b as any).scripts as ReadonlyArray - if (as.length !== bs.length) return false - for (let i = 0; i < as.length; i++) if (!equals(as[i], bs[i])) return false - return true - } - case "atLeast": { - const bs = b as any - if (a.required !== bs.required) return false - const as = a.scripts - const bscripts = bs.scripts as ReadonlyArray - if (as.length !== bscripts.length) return false - for (let i = 0; i < as.length; i++) if (!equals(as[i], bscripts[i])) return false - return true - } - } -} - -/** - * CBOR bytes transformation schema for Native. - * Transforms between CBOR bytes and Native using CBOR encoding. - * - * @since 2.0.0 - * @category schemas - */ -export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => - Schema.compose( - CBOR.FromBytes(options), // Uint8Array → CBOR - FromCDDL // CBOR → Native - ).annotations({ - identifier: "Native.FromCBORBytes", - title: "Native from CBOR Bytes", - description: "Transforms CBOR bytes to Native" - }) - -/** - * CBOR hex transformation schema for Native. - * Transforms between CBOR hex string and Native using CBOR encoding. - * - * @since 2.0.0 - * @category schemas - */ -export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => - Schema.compose( - Schema.Uint8ArrayFromHex, // string → Uint8Array - FromCBORBytes(options) // Uint8Array → Native - ).annotations({ - identifier: "Native.FromCBORHex", - title: "Native from CBOR Hex", - description: "Transforms CBOR hex string to Native" - }) - -/** - * Root Functions - * ============================================================================ - */ - -/** - * Parse Native from CBOR bytes. - * - * @since 2.0.0 - * @category parsing - */ -export const fromCBORBytes = Function.makeCBORDecodeSync(FromCDDL, NativeError, "NativeScripts.fromCBORBytes") - -/** - * Parse Native from CBOR hex string. - * - * @since 2.0.0 - * @category parsing - */ -export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Native => - E.getOrThrow(Either.fromCBORHex(hex, options)) - -/** - * Encode Native to CBOR bytes. - * - * @since 2.0.0 - * @category encoding - */ -export const toCBORBytes = Function.makeCBOREncodeSync(FromCDDL, NativeError, "Native.toCBORBytes") - -/** - * Encode Native to CBOR hex string. - * - * @since 2.0.0 - * @category encoding - */ -export const toCBORHex = Function.makeCBOREncodeHexSync(FromCDDL, NativeError, "Native.toCBORHex") - -// ============================================================================ -// Effect Namespace -// ============================================================================ - -/** - * Effect-based error handling variants for functions that can fail. - * - * @since 2.0.0 - * @category effect - */ -export namespace Either { - /** - * Parse Native from CBOR bytes with Effect error handling. - * - * @since 2.0.0 - * @category parsing - */ - export const fromCBORBytes = Function.makeCBORDecodeEither(FromCDDL, NativeError) - - /** - * Parse Native from CBOR hex string with Effect error handling. - * - * @since 2.0.0 - * @category parsing - */ - export const fromCBORHex = Function.makeCBORDecodeHexEither(FromCDDL, NativeError) - - /** - * Encode Native to CBOR bytes with Effect error handling. - * - * @since 2.0.0 - * @category encoding - */ - export const toCBORBytes = Function.makeCBOREncodeEither(FromCDDL, NativeError) - - /** - * Encode Native to CBOR hex string with Effect error handling. - * - * @since 2.0.0 - * @category encoding - */ - export const toCBORHex = Function.makeCBOREncodeHexEither(FromCDDL, NativeError) -} diff --git a/packages/evolution/src/UTxO.ts b/packages/evolution/src/UTxO.ts index 157e06ae..d3a8d548 100644 --- a/packages/evolution/src/UTxO.ts +++ b/packages/evolution/src/UTxO.ts @@ -190,3 +190,14 @@ export const toArray = (set: UTxOSet): Array => Array.from(set) * @category getters */ export const toOutRefString = (utxo: UTxO): string => `${TransactionHash.toHex(utxo.transactionId)}#${utxo.index}` + +/** + * Sum all assets across an array (or Set) of UTxOs. + * + * @since 2.0.0 + * @category aggregation + */ +export const totalAssets = (utxos: ReadonlyArray | Set): Assets.Assets => { + const arr = (globalThis.Array.isArray(utxos) ? utxos : globalThis.Array.from(utxos)) as ReadonlyArray + return arr.reduce((acc: Assets.Assets, utxo: UTxO) => Assets.merge(acc, utxo.assets), Assets.zero) +} diff --git a/packages/evolution/src/index.ts b/packages/evolution/src/index.ts index ba5fd6ac..06f12627 100644 --- a/packages/evolution/src/index.ts +++ b/packages/evolution/src/index.ts @@ -98,8 +98,11 @@ export * as Script from "./Script.js" export * as ScriptDataHash from "./ScriptDataHash.js" export * as ScriptHash from "./ScriptHash.js" export * as ScriptRef from "./ScriptRef.js" +export * from "./sdk/client/Capabilities.js" +export * from "./sdk/client/Chain.js" export * from "./sdk/client/Client.js" export { createClient } from "./sdk/client/ClientImpl.js" +export * as EvalRedeemer from "./sdk/EvalRedeemer.js" export * as SingleHostAddr from "./SingleHostAddr.js" export * as SingleHostName from "./SingleHostName.js" export * as StakeReference from "./StakeReference.js" diff --git a/packages/evolution/src/sdk/builders/SignBuilder.ts b/packages/evolution/src/sdk/builders/SignBuilder.ts index d9108bca..1135d3e4 100644 --- a/packages/evolution/src/sdk/builders/SignBuilder.ts +++ b/packages/evolution/src/sdk/builders/SignBuilder.ts @@ -3,61 +3,129 @@ import type { Effect } from "effect" import type * as Transaction from "../../Transaction.js" import type * as TransactionHash from "../../TransactionHash.js" import type * as TransactionWitnessSet from "../../TransactionWitnessSet.js" -import type { EffectToPromiseAPI } from "../Type.js" -import type { SubmitBuilder } from "./SubmitBuilder.js" +import type { SubmitTx, WalletSubmit } from "../client/Capabilities.js" +import type { SubmitBuilder, SubmitBuilderOf } from "./SubmitBuilder.js" import type { ChainResult, TransactionBuilderError } from "./TransactionBuilder.js" import type { TransactionResultBase } from "./TransactionResult.js" // ============================================================================ -// Progressive Builder Interfaces +// Effect-layer interfaces // ============================================================================ /** - * Effect-based API for SignBuilder operations. + * Effect-based API for SignBuilder operations — does not include `signAndSubmit`. * - * Includes all TransactionResultBase.Effect methods plus signing-specific operations. + * `signAndSubmit` lives on SignBuilderSubmittableEffect (added when C has submit capability). * - * @since 2.0.0 + * @typeParam C - Client capabilities (determines SubmitBuilderOf return type) + * + * @since 2.1.0 * @category interfaces */ -export interface SignBuilderEffect { - // Base transaction methods (from TransactionResultBase) +export interface SignBuilderEffect { readonly toTransaction: () => Effect.Effect readonly toTransactionWithFakeWitnesses: () => Effect.Effect readonly estimateFee: () => Effect.Effect - - // Signing methods - readonly sign: () => Effect.Effect - readonly signAndSubmit: () => Effect.Effect + readonly sign: () => Effect.Effect, TransactionBuilderError> readonly signWithWitness: ( witnessSet: TransactionWitnessSet.TransactionWitnessSet - ) => Effect.Effect + ) => Effect.Effect, TransactionBuilderError> readonly assemble: ( witnesses: ReadonlyArray - ) => Effect.Effect + ) => Effect.Effect, TransactionBuilderError> readonly partialSign: () => Effect.Effect readonly getWitnessSet: () => Effect.Effect } /** - * SignBuilder extends TransactionResultBase with signing capabilities. + * Extended Effect-based API adding `signAndSubmit` — only when C has submit capability. * - * Only available when the client has a signing wallet (seed, private key, or API wallet). - * Provides access to unsigned transaction (via base interface) and signing operations. + * Accessible via SignBuilderOf when C extends SubmitTx | WalletSubmit. * - * Includes `chainResult` for transaction chaining - use `chainResult.available` as - * `availableUtxos` for the next transaction in a chain. + * @typeParam C - Client capabilities * - * @since 2.0.0 + * @since 2.1.0 * @category interfaces */ -export interface SignBuilder extends TransactionResultBase, EffectToPromiseAPI { - readonly Effect: SignBuilderEffect +export interface SignBuilderSubmittableEffect extends SignBuilderEffect { + readonly signAndSubmit: () => Effect.Effect +} + +// ============================================================================ +// Promise-layer interfaces (explicit listing — conditional types cannot be used in extends) +// ============================================================================ + +/** + * Base SignBuilder available whenever the client has Signable capability. + * + * `sign()`, `signWithWitness()`, and `assemble()` return `SubmitBuilderOf`: + * - `SubmitBuilder` (with `.submit()`) when C extends SubmitTx | WalletSubmit + * - `SubmitBuilderBase` (witness set only) otherwise + * + * Does NOT include `signAndSubmit()` — that lives on SignBuilder when C can submit. + * + * @typeParam C - Client capabilities + * + * @since 2.1.0 + * @category interfaces + */ +export interface SignBuilderBase extends TransactionResultBase { + readonly Effect: SignBuilderEffect /** * Compute chain result for building dependent transactions. * Contains consumed UTxOs, available UTxOs (remaining + created), and txHash. * - * Result is memoized - computed once on first call, cached for subsequent calls. + * Result is memoized — computed once on first call, cached for subsequent calls. */ readonly chainResult: () => ChainResult + readonly sign: () => Promise> + readonly signWithWitness: ( + witnessSet: TransactionWitnessSet.TransactionWitnessSet + ) => Promise> + readonly assemble: ( + witnesses: ReadonlyArray + ) => Promise> + readonly partialSign: () => Promise + readonly getWitnessSet: () => Promise } + +/** + * Full SignBuilder — extends SignBuilderBase with `signAndSubmit()`. + * + * Only directly accessible when C extends SubmitTx | WalletSubmit, enforced via + * `SignBuilderOf`. Default `C = SubmitTx & WalletSubmit` preserves backward + * compatibility for code that uses `SignBuilder` without a type argument. + * + * @typeParam C - Client capabilities (defaults to SubmitTx & WalletSubmit for backwards compat) + * + * @since 2.0.0 + * @category interfaces + */ +export interface SignBuilder extends SignBuilderBase { + readonly Effect: SignBuilderSubmittableEffect + /** + * Sign and submit the transaction in one step. + * + * Convenience method combining sign() + submit(). + * Only available when C has SubmitTx or WalletSubmit capability. + * + * @since 2.0.0 + */ + readonly signAndSubmit: () => Promise +} + +/** + * Conditional sign builder type based on client submit capability. + * + * - C extends SubmitTx | WalletSubmit → SignBuilder (includes signAndSubmit) + * - otherwise → SignBuilderBase (no signAndSubmit, sign returns SubmitBuilderBase) + * + * Used as the result type of TxBuilder.build() and assembleFinalResult. + * + * @since 2.1.0 + * @category builder-types + */ +export type SignBuilderOf = C extends SubmitTx | WalletSubmit ? SignBuilder : SignBuilderBase + +// Re-export SubmitBuilder for convenience (used by callers that import from this module) +export type { SubmitBuilder } diff --git a/packages/evolution/src/sdk/builders/SubmitBuilder.ts b/packages/evolution/src/sdk/builders/SubmitBuilder.ts index 26ed4be9..50aa03bd 100644 --- a/packages/evolution/src/sdk/builders/SubmitBuilder.ts +++ b/packages/evolution/src/sdk/builders/SubmitBuilder.ts @@ -12,9 +12,31 @@ import type { Effect } from "effect" import type * as TransactionHash from "../../TransactionHash.js" import type * as TransactionWitnessSet from "../../TransactionWitnessSet.js" +import type { SubmitTx, WalletSubmit } from "../client/Capabilities.js" import type { EffectToPromiseAPI } from "../Type.js" import type { TransactionBuilderError } from "./TransactionBuilder.js" +/** + * Base result after signing — always available regardless of submit capability. + * + * Provides access to the witness set for multi-party signing, hardware wallet + * workflows, and other scenarios where submission happens separately. + * + * @since 2.1.0 + * @category interfaces + */ +export interface SubmitBuilderBase { + /** + * The witness set containing all signatures for this transaction. + * + * Can be used to inspect the signatures or combine with other witness sets + * for multi-party signing scenarios. + * + * @since 2.0.0 + */ + readonly witnessSet: TransactionWitnessSet.TransactionWitnessSet +} + /** * Effect-based API for SubmitBuilder operations. * @@ -34,28 +56,28 @@ export interface SubmitBuilderEffect { /** * SubmitBuilder - represents a signed transaction ready for submission. * - * The final stage in the transaction lifecycle after building and signing. - * Provides the submit() method to broadcast the transaction to the blockchain - * and retrieve the transaction hash. + * Extends SubmitBuilderBase with submit capability. Only accessible when the + * client has SubmitTx or WalletSubmit capability (enforced via SubmitBuilderOf). * * @since 2.0.0 * @category interfaces */ -export interface SubmitBuilder extends EffectToPromiseAPI { +export interface SubmitBuilder extends SubmitBuilderBase, EffectToPromiseAPI { /** * Effect-based API for compositional workflows. * * @since 2.0.0 */ readonly Effect: SubmitBuilderEffect - - /** - * The witness set containing all signatures for this transaction. - * - * Can be used to inspect the signatures or combine with other witness sets - * for multi-party signing scenarios. - * - * @since 2.0.0 - */ - readonly witnessSet: TransactionWitnessSet.TransactionWitnessSet } + +/** + * Conditional submit builder: includes submit() when C has SubmitTx or WalletSubmit capability. + * + * Used as the return type of SignBuilder.sign(), SignBuilder.signWithWitness(), and + * SignBuilder.assemble() to enforce capability constraints at compile time. + * + * @since 2.1.0 + * @category builder-types + */ +export type SubmitBuilderOf = C extends SubmitTx | WalletSubmit ? SubmitBuilder : SubmitBuilderBase diff --git a/packages/evolution/src/sdk/builders/TransactionBuilder.ts b/packages/evolution/src/sdk/builders/TransactionBuilder.ts index 4b20f9cc..4fe879ef 100644 --- a/packages/evolution/src/sdk/builders/TransactionBuilder.ts +++ b/packages/evolution/src/sdk/builders/TransactionBuilder.ts @@ -45,14 +45,25 @@ import * as Time from "../../Time/index.js" import * as Transaction from "../../Transaction.js" import type * as TxOut from "../../TxOut.js" import { runEffectPromise } from "../../utils/effect-runtime.js" +import { calculateTransactionSize } from "../../utils/FeeValidation.js" import type * as CoreUTxO from "../../UTxO.js" import type * as VotingProcedures from "../../VotingProcedures.js" +import type { + Addressable, + EvaluateTx as EvaluateTxCapability, + QueryProtocolParams, + QueryUtxos, + Signable, +} from "../client/Capabilities.js" import type { EvalRedeemer } from "../EvalRedeemer.js" import type * as Provider from "../provider/Provider.js" -import type * as WalletNew from "../wallet/WalletNew.js" +import type * as Wallet from "../wallet/Wallet.js" import type { CoinSelectionAlgorithm, CoinSelectionFunction } from "./CoinSelection.js" +import { buildFakeWitnessSet } from "./internal/FeeEstimation.js" +import { makeSignBuilder } from "./internal/SignBuilderImpl.js" +import { assembleTransaction, buildTransactionInputs } from "./internal/TxAssembly.js" import { createAddSignerProgram } from "./operations/AddSigner.js" -import { attachScriptToState } from "./operations/Attach.js" +import { createAttachScriptProgram } from "./operations/Attach.js" import { createAttachMetadataProgram } from "./operations/AttachMetadata.js" import { createCollectFromProgram } from "./operations/Collect.js" import { @@ -114,17 +125,10 @@ import { executeEvaluation } from "./phases/Evaluation.js" import { executeFallback } from "./phases/Fallback.js" import { executeFeeCalculation } from "./phases/FeeCalculation.js" import { executeSelection } from "./phases/Selection.js" -import type { DeferredRedeemer } from "./RedeemerBuilder.js" -import type { SignBuilder } from "./SignBuilder.js" -import { makeSignBuilder } from "./SignBuilderImpl.js" +import type { DeferredRedeemer, RedeemerArg } from "./RedeemerBuilder.js" +import type { SignBuilder, SignBuilderOf } from "./SignBuilder.js" import type { TransactionResultBase } from "./TransactionResult.js" import { makeTransactionResult } from "./TransactionResult.js" -import { - assembleTransaction, - buildFakeWitnessSet, - buildTransactionInputs, - calculateTransactionSize -} from "./TxBuilderImpl.js" /** * Error type for failures occurring during transaction builder operations. @@ -225,7 +229,7 @@ const resolveProtocolParameters = ( const resolveChangeAddress = ( config: TxBuilderConfig, options?: BuildOptions -): Effect.Effect => { +): Effect.Effect => { if (options?.changeAddress) { return Effect.succeed(options.changeAddress) } @@ -251,7 +255,7 @@ const resolveAvailableUtxos = ( options?: BuildOptions ): Effect.Effect< ReadonlyArray, - TransactionBuilderError | WalletNew.WalletError | Provider.ProviderError + TransactionBuilderError | Wallet.WalletError | Provider.ProviderError > => { if (options?.availableUtxos) { return Effect.succeed(options.availableUtxos) @@ -449,7 +453,7 @@ const assembleFinalResult = ( const wallet = config.wallet - if (wallet?.type === "signing" || wallet?.type === "api") { + if (hasSigningCapability(wallet)) { return makeSignBuilder({ transaction, transactionWithFakeWitnesses: txWithFakeWitnesses, @@ -516,7 +520,7 @@ const assembleAndValidateTransaction = Effect.gen(function* () { ) // Build transaction inputs and assemble transaction body - const inputs = yield* buildTransactionInputs(selectedUtxos) + const inputs = buildTransactionInputs(selectedUtxos) const transaction = yield* assembleTransaction(inputs, allOutputs, buildCtx.calculatedFee) // SAFETY CHECK: Validate transaction size against protocol limit @@ -533,7 +537,7 @@ const assembleAndValidateTransaction = Effect.gen(function* () { auxiliaryData: finalState.auxiliaryData ?? null }) - const txSizeWithWitnesses = yield* calculateTransactionSize(txWithFakeWitnesses) + const txSizeWithWitnesses = calculateTransactionSize(txWithFakeWitnesses) const protocolParams = yield* ProtocolParametersTag yield* Effect.logDebug( @@ -1205,6 +1209,59 @@ export interface ProtocolParameters { // maxCollateralInputs?: number } +// ── Structural "Like" types ───────────────────────────────────────────────── +// These define the minimum shape the builder actually needs from a provider +// or wallet. The full Provider/Wallet interfaces satisfy these structurally, +// and so does the pipe Client's Effect namespace — no adapter needed. + +/** + * Structural subset of Provider that the transaction builder actually needs. + * Both `Provider` and `{ Effect: pipeClient.Effect }` satisfy this. + * + * @since 2.1.0 + * @category config + */ +export interface ProviderLike { + readonly Effect: Pick< + Provider.ProviderEffect, + "getProtocolParameters" | "getUtxos" | "evaluateTx" | "submitTx" + > +} + +/** + * Structural subset of a read-only wallet for the transaction builder. + * Provides change address resolution without signing. + * + * @since 2.1.0 + * @category config + */ +export interface ReadOnlyWalletLike { + readonly Effect: Pick +} + +/** + * Structural subset of a signing wallet for the transaction builder. + * Provides change address resolution and transaction signing. + * + * @since 2.1.0 + * @category config + */ +export interface SigningWalletLike { + readonly Effect: Pick & + Pick +} + +/** + * Union of wallet-like types accepted by TxBuilderConfig. + * + * @since 2.1.0 + * @category config + */ +export type WalletLike = ReadOnlyWalletLike | SigningWalletLike + +const hasSigningCapability = (wallet: WalletLike | undefined): wallet is SigningWalletLike => + wallet !== undefined && "signTx" in wallet.Effect + /** * Configuration for TransactionBuilder. * Immutable configuration passed to builder at creation time. @@ -1231,12 +1288,11 @@ export interface TxBuilderConfig { * When provided: Automatic change address and UTxO resolution. * When omitted: Must provide changeAddress and availableUtxos in BuildOptions. * - * ReadOnlyWallet: For read-only clients that can build but not sign transactions. - * SigningWallet/ApiWallet: For signing clients with full transaction signing capability. + * Accepts full Wallet interfaces or structural WalletLike subsets. * * Override per-build via BuildOptions.changeAddress and BuildOptions.availableUtxos. */ - readonly wallet?: WalletNew.SigningWallet | WalletNew.ApiWallet | WalletNew.ReadOnlyWallet + readonly wallet?: WalletLike /** * Optional provider for: @@ -1244,10 +1300,9 @@ export interface TxBuilderConfig { * - Transaction submission (provider.Effect.submitTx) * - Protocol parameters * - * Works together with wallet to provide everything needed for transaction building. - * When wallet is omitted, provider is only used if you call provider methods directly. + * Accepts full Provider or structural ProviderLike subset. */ - readonly provider?: Provider.Provider + readonly provider?: ProviderLike /** * Network type for slot configuration in script evaluation. @@ -1557,8 +1612,8 @@ export type ProgramStep = Effect.Effect = W extends - | WalletNew.SigningWallet - | WalletNew.ApiWallet + | Wallet.SigningWallet + | Wallet.ApiWallet ? SignBuilder : TransactionResultBase @@ -2297,6 +2352,8 @@ export interface TransactionBuilderBase { * This builder type is returned when makeTxBuilder() is called with a signing wallet. * Type narrowing happens automatically at construction time - no call-site guards needed. * + * @deprecated Use TxBuilder from newTx(client) instead. + * * @since 2.0.0 * @category builder-interfaces */ @@ -2329,7 +2386,7 @@ export interface SigningTransactionBuilder extends TransactionBuilderBase { options?: BuildOptions ) => Effect.Effect< SignBuilder, - TransactionBuilderError | EvaluationError | WalletNew.WalletError | Provider.ProviderError, + TransactionBuilderError | EvaluationError | Wallet.WalletError | Provider.ProviderError, never > @@ -2347,7 +2404,7 @@ export interface SigningTransactionBuilder extends TransactionBuilderBase { readonly buildEither: ( options?: BuildOptions ) => Promise< - Either + Either > } @@ -2360,6 +2417,8 @@ export interface SigningTransactionBuilder extends TransactionBuilderBase { * This builder type is returned when makeTxBuilder() is called with a read-only wallet or no wallet. * Type narrowing happens automatically at construction time - no call-site guards needed. * + * @deprecated Use TxBuilder from newTx(client) instead. + * * @since 2.0.0 * @category builder-interfaces */ @@ -2392,7 +2451,7 @@ export interface ReadOnlyTransactionBuilder extends TransactionBuilderBase { options?: BuildOptions ) => Effect.Effect< TransactionResultBase, - TransactionBuilderError | EvaluationError | WalletNew.WalletError | Provider.ProviderError, + TransactionBuilderError | EvaluationError | Wallet.WalletError | Provider.ProviderError, never > @@ -2412,7 +2471,7 @@ export interface ReadOnlyTransactionBuilder extends TransactionBuilderBase { ) => Promise< Either< TransactionResultBase, - TransactionBuilderError | EvaluationError | WalletNew.WalletError | Provider.ProviderError + TransactionBuilderError | EvaluationError | Wallet.WalletError | Provider.ProviderError > > } @@ -2421,233 +2480,541 @@ export interface ReadOnlyTransactionBuilder extends TransactionBuilderBase { * Union type for all transaction builders. * Use specific types (SigningTransactionBuilder or ReadOnlyTransactionBuilder) when you know the wallet type. * + * @deprecated Use TxBuilder from newTx(client) instead. + * * @since 2.0.0 * @category builder-interfaces */ export type TransactionBuilder = SigningTransactionBuilder | ReadOnlyTransactionBuilder /** - * Conditional type to determine the correct TransactionBuilder based on wallet type. - * - If wallet is SigningWallet or ApiWallet: SigningTransactionBuilder - * - If wallet is ReadOnlyWallet or undefined: ReadOnlyTransactionBuilder + * Conditional type to determine the correct TransactionBuilder based on wallet capabilities. + * - If wallet exposes signTx: SigningTransactionBuilder + * - Otherwise: ReadOnlyTransactionBuilder * * @internal */ export type TxBuilderResultType< - W extends WalletNew.SigningWallet | WalletNew.ApiWallet | WalletNew.ReadOnlyWallet | undefined -> = W extends WalletNew.SigningWallet | WalletNew.ApiWallet ? SigningTransactionBuilder : ReadOnlyTransactionBuilder + W extends WalletLike | undefined +> = W extends { readonly Effect: Pick } + ? SigningTransactionBuilder + : ReadOnlyTransactionBuilder + +// ============================================================================ +// TxBuilder — Capability-Tracked Builder Interface +// ============================================================================ /** - * Construct a TransactionBuilder instance from protocol configuration. + * Computes which fields MUST be supplied in build(options) because C cannot provide them. * - * The builder accumulates chainable method calls as deferred ProgramSteps. Calling build() or chain() - * creates fresh state (new Refs) and executes all accumulated programs sequentially, ensuring - * no state pollution between invocations. + * Static requirements (QPP, Addressable, QueryUtxos): required when C lacks the interface. + * Dynamic requirement (EvaluateTx): required when R has accumulated EvaluateTx AND C + * doesn't provide EvaluateTx capability. Uses `{} extends R` (not `C extends R`) to + * correctly handle the `unknown` edge case — `unknown extends {}` is false in TypeScript, + * which would incorrectly mark evaluator as required even when R = {} (no requirements). * - * The return type is determined by the actual wallet provided using conditional types: - * - SigningTransactionBuilder: When wallet is SigningWallet or ApiWallet - * - ReadOnlyTransactionBuilder: When wallet is ReadOnlyWallet or undefined + * @internal + */ +type RequiredBuildInputs = + & (C extends QueryProtocolParams ? {} : { readonly protocolParameters: ProtocolParameters }) + & (C extends Addressable ? {} : { readonly changeAddress: CoreAddress.Address }) + & (C extends QueryUtxos ? {} : { readonly availableUtxos: ReadonlyArray }) + & ({} extends R ? {} : C extends EvaluateTxCapability ? {} : { readonly evaluator: Evaluator }) + +/** + * Resolves to `[options?: BuildOptions]` when C satisfies all requirements, + * or `[options: RequiredBuildInputs & BuildOptions]` when fields are missing. * - * Wallet type narrowing happens at construction time based on the wallet's actual type. - * No call-site type narrowing or type guards needed. + * @internal + */ +type BuildArgs = + {} extends RequiredBuildInputs + ? [options?: BuildOptions] + : [options: RequiredBuildInputs & BuildOptions] + +/** + * Result type for TxBuilder.build(): SignBuilder when C has signing capability, TransactionResultBase otherwise. * - * Wallet parameter is optional; if omitted, changeAddress and availableUtxos must be - * provided at build time via BuildOptions. + * @since 2.1.0 + * @category builder-types + */ +export type TxBuildResult = C extends Signable ? SignBuilderOf : TransactionResultBase + +/** + * Capability-tracked transaction builder. * - * @since 2.0.0 - * @category constructors + * `C` (client capabilities) is fixed at `newTx(client)` construction and never changes. + * `R` (accumulated requirements) starts as `{}` and grows via `R & EvaluateTxCapability` + * each time an operation with a redeemer (but no inline exUnits) is added. + * + * At build time, `BuildArgs` computes: + * - Which `BuildOptions` fields are REQUIRED because C cannot provide them + * - Whether `evaluator` is required based on R and C + * + * When `{} extends RequiredBuildInputs` (C provides everything), `build()` can be + * called with no arguments. + * + * `compose(other)` imports the composed builder's programs AND merges R2 into R, + * so script requirements from fragments are never silently dropped. * + * @typeParam C - Client capabilities (fixed at newTx construction) + * @typeParam R - Accumulated dynamic requirements (grows with redeemer operations) + * + * @since 2.1.0 + * @category builder-interfaces */ -export function makeTxBuilder< - W extends WalletNew.SigningWallet | WalletNew.ApiWallet | WalletNew.ReadOnlyWallet | undefined ->(config: Partial & { wallet?: W }): TxBuilderResultType -export function makeTxBuilder(config: TxBuilderConfig) { - const programs: Array = [] +export interface TxBuilder { + // ── Operations with no new requirements ──────────────────────────────────── + payToAddress(params: PayToAddressParams): TxBuilder + attachScript(params: { script: CoreScript.Script }): TxBuilder + readFrom(params: ReadFromParams): TxBuilder + setValidity(params: ValidityParams): TxBuilder + addSigner(params: AddSignerParams): TxBuilder + sendAll(params: SendAllParams): TxBuilder + attachMetadata(params: AttachMetadataParams): TxBuilder + registerStake(params: RegisterStakeParams): TxBuilder + registerPool(params: RegisterPoolParams): TxBuilder + retirePool(params: RetirePoolParams): TxBuilder + registerDRep(params: RegisterDRepParams): TxBuilder + updateDRep(params: UpdateDRepParams): TxBuilder + deregisterDRep(params: DeregisterDRepParams): TxBuilder + authCommitteeHot(params: AuthCommitteeHotParams): TxBuilder + resignCommitteeCold(params: ResignCommitteeColdParams): TxBuilder + registerAndDelegateTo(params: RegisterAndDelegateToParams): TxBuilder + /** @deprecated Use delegateToPool, delegateToDRep, or delegateToPoolAndDRep instead */ + delegateTo(params: DelegateToParams): TxBuilder + delegateToPool(params: DelegateToPoolParams): TxBuilder + delegateToDRep(params: DelegateToDRepParams): TxBuilder + delegateToPoolAndDRep(params: DelegateToPoolAndDRepParams): TxBuilder + vote(params: VoteParams): TxBuilder + propose(params: ProposeParams): TxBuilder + + // ── Operations that may add EvaluateTxCapability to R (most-specific overload first) ── + // Most-specific (redeemer present) must come first so TypeScript picks the right overload. + + /** redeemer without exUnits → R grows to R & EvaluateTxCapability */ + collectFrom(params: CollectFromParams & { readonly redeemer: RedeemerArg }): TxBuilder + /** no redeemer → R unchanged */ + collectFrom(params: CollectFromParams): TxBuilder + + /** redeemer without exUnits → R grows to R & EvaluateTxCapability */ + mintAssets(params: MintTokensParams & { readonly redeemer: RedeemerArg }): TxBuilder + /** no redeemer → R unchanged */ + mintAssets(params: MintTokensParams): TxBuilder + + /** redeemer without exUnits → R grows to R & EvaluateTxCapability */ + withdraw(params: WithdrawParams & { readonly redeemer: RedeemerArg }): TxBuilder + /** no redeemer → R unchanged */ + withdraw(params: WithdrawParams): TxBuilder + + /** redeemer without exUnits → R grows to R & EvaluateTxCapability */ + deregisterStake(params: DeregisterStakeParams & { readonly redeemer: RedeemerArg }): TxBuilder + /** no redeemer → R unchanged */ + deregisterStake(params: DeregisterStakeParams): TxBuilder + + // ── Compose and introspect ──────────────────────────────────────────────── + /** + * Merge another builder's programs into this one. + * + * C (capabilities) is fixed from the receiver — the composed builder's client + * capabilities are NOT imported. R2 IS merged (via &) so script requirements + * from fragments are never silently dropped. + */ + compose(other: TxBuilder): TxBuilder - const txBuilder = { - // ============================================================================ - // Chainable builder methods - Create ProgramSteps, return same instance - // ============================================================================ + /** Snapshot of accumulated programs, for composition and introspection. */ + getPrograms(): ReadonlyArray - payToAddress: (params: PayToAddressParams) => { - // Create ProgramStep for deferred execution - const program = createPayToAddressProgram(params) + // ── Build methods ───────────────────────────────────────────────────────── + /** + * Build the transaction. + * + * When `C` satisfies all requirements and `R` is `{}`, can be called with no arguments. + * Otherwise, `options` becomes required with the missing fields. + */ + build(...args: BuildArgs): Promise> + + buildEffect(...args: BuildArgs): Effect.Effect< + TxBuildResult, + TransactionBuilderError | EvaluationError | Wallet.WalletError | Provider.ProviderError, + never + > + + buildEither(...args: BuildArgs): Promise< + Either< + TxBuildResult, + TransactionBuilderError | EvaluationError | Wallet.WalletError | Provider.ProviderError + > + > +} + +type BuilderCompletionError = + | TransactionBuilderError + | EvaluationError + | Wallet.WalletError + | Provider.ProviderError + +type BuilderCoreResult = SignBuilder | TransactionResultBase + +interface BuilderCore { + readonly config: TxBuilderConfig + readonly appendProgram: (program: ProgramStep) => void + readonly appendPrograms: (programs: ReadonlyArray) => void + readonly getPrograms: () => ReadonlyArray + readonly buildEffect: ( + options?: BuildOptions + ) => Effect.Effect + readonly build: (options?: BuildOptions) => Promise + readonly buildEither: ( + options?: BuildOptions + ) => Promise> + readonly buildPartialEffect: ( + options?: BuildOptions + ) => Effect.Effect + readonly buildPartial: (options?: BuildOptions) => Promise +} + +const provideBuildLogging = ( + effect: Effect.Effect, + debug: boolean | undefined +): Effect.Effect => + debug + ? effect.pipe(Effect.provide(Layer.merge(Logger.pretty, Logger.minimumLogLevel(LogLevel.Debug)))) + : effect + +const createBuilderCore = (config: TxBuilderConfig): BuilderCore => { + const programs: Array = [] + + return { + config, + appendProgram: (program) => { programs.push(program) - return txBuilder // Return same instance for chaining }, + appendPrograms: (steps) => { + if (steps.length > 0) { + programs.push(...steps) + } + }, + getPrograms: () => [...programs], + buildEffect: (options?: BuildOptions) => + makeBuild(config, programs, options) as Effect.Effect, + build: (options?: BuildOptions) => + runEffectPromise(provideBuildLogging(makeBuild(config, programs, options), options?.debug)) as Promise, + buildEither: (options?: BuildOptions) => + runEffectPromise( + provideBuildLogging(makeBuild(config, programs, options).pipe(Effect.either), options?.debug) + ) as Promise>, + buildPartialEffect: (options?: BuildOptions) => buildPartialEffectCore(config, programs, options), + buildPartial: (options?: BuildOptions) => runEffectPromise(buildPartialEffectCore(config, programs, options)) + } +} +const createLegacyBuilderFromCore = (core: BuilderCore): TxBuilderResultType => { + const txBuilder = { + payToAddress: (params: PayToAddressParams) => { + core.appendProgram(createPayToAddressProgram(params)) + return txBuilder + }, collectFrom: (params: CollectFromParams) => { - // Create ProgramStep for deferred execution - const program = createCollectFromProgram(params) - programs.push(program) - return txBuilder // Return same instance for chaining + core.appendProgram(createCollectFromProgram(params)) + return txBuilder }, - sendAll: (params: SendAllParams) => { - // Create ProgramStep for deferred execution - const program = createSendAllProgram(params) - programs.push(program) - return txBuilder // Return same instance for chaining + core.appendProgram(createSendAllProgram(params)) + return txBuilder }, - mintAssets: (params: MintTokensParams) => { - // Create ProgramStep for deferred execution - const program = createMintAssetsProgram(params) - programs.push(program) - return txBuilder // Return same instance for chaining + core.appendProgram(createMintAssetsProgram(params)) + return txBuilder }, - readFrom: (params: ReadFromParams) => { - // Create ProgramStep for deferred execution - const program = createReadFromProgram(params) - programs.push(program) - return txBuilder // Return same instance for chaining + core.appendProgram(createReadFromProgram(params)) + return txBuilder }, - attachScript: (params: { script: CoreScript.Script }) => { - // Create ProgramStep for deferred execution - const program = attachScriptToState(params.script) - programs.push(program) - return txBuilder // Return same instance for chaining + core.appendProgram(createAttachScriptProgram(params.script)) + return txBuilder }, - - // Staking/Certificate methods registerStake: (params: RegisterStakeParams) => { - const program = createRegisterStakeProgram(params) - programs.push(program) + core.appendProgram(createRegisterStakeProgram(params)) return txBuilder }, deregisterStake: (params: DeregisterStakeParams) => { - const program = createDeregisterStakeProgram(params) - programs.push(program) + core.appendProgram(createDeregisterStakeProgram(params)) return txBuilder }, delegateTo: (params: DelegateToParams) => { - const program = createDelegateToProgram(params) - programs.push(program) + core.appendProgram(createDelegateToProgram(params)) return txBuilder }, delegateToPool: (params: DelegateToPoolParams) => { - const program = createDelegateToPoolProgram(params) - programs.push(program) + core.appendProgram(createDelegateToPoolProgram(params)) return txBuilder }, delegateToDRep: (params: DelegateToDRepParams) => { - const program = createDelegateToDRepProgram(params) - programs.push(program) + core.appendProgram(createDelegateToDRepProgram(params)) return txBuilder }, delegateToPoolAndDRep: (params: DelegateToPoolAndDRepParams) => { - const program = createDelegateToPoolAndDRepProgram(params) - programs.push(program) + core.appendProgram(createDelegateToPoolAndDRepProgram(params)) return txBuilder }, withdraw: (params: WithdrawParams) => { - const program = createWithdrawProgram(params, config) - programs.push(program) + core.appendProgram(createWithdrawProgram(params)) return txBuilder }, registerAndDelegateTo: (params: RegisterAndDelegateToParams) => { - const program = createRegisterAndDelegateToProgram(params) - programs.push(program) + core.appendProgram(createRegisterAndDelegateToProgram(params)) return txBuilder }, registerDRep: (params: RegisterDRepParams) => { - const program = createRegisterDRepProgram(params) - programs.push(program) + core.appendProgram(createRegisterDRepProgram(params)) return txBuilder }, updateDRep: (params: UpdateDRepParams) => { - const program = createUpdateDRepProgram(params) - programs.push(program) + core.appendProgram(createUpdateDRepProgram(params)) return txBuilder }, deregisterDRep: (params: DeregisterDRepParams) => { - const program = createDeregisterDRepProgram(params) - programs.push(program) + core.appendProgram(createDeregisterDRepProgram(params)) return txBuilder }, authCommitteeHot: (params: AuthCommitteeHotParams) => { - const program = createAuthCommitteeHotProgram(params) - programs.push(program) + core.appendProgram(createAuthCommitteeHotProgram(params)) return txBuilder }, resignCommitteeCold: (params: ResignCommitteeColdParams) => { - const program = createResignCommitteeColdProgram(params) - programs.push(program) + core.appendProgram(createResignCommitteeColdProgram(params)) return txBuilder }, registerPool: (params: RegisterPoolParams) => { - const program = createRegisterPoolProgram(params) - programs.push(program) + core.appendProgram(createRegisterPoolProgram(params)) return txBuilder }, retirePool: (params: RetirePoolParams) => { - const program = createRetirePoolProgram(params) - programs.push(program) + core.appendProgram(createRetirePoolProgram(params)) return txBuilder }, setValidity: (params: ValidityParams) => { - programs.push(createSetValidityProgram(params)) + core.appendProgram(createSetValidityProgram(params)) return txBuilder }, vote: (params: VoteParams) => { - const program = createVoteProgram(params) - programs.push(program) + core.appendProgram(createVoteProgram(params)) return txBuilder }, propose: (params: ProposeParams) => { - const program = createProposeProgram(params) - programs.push(program) + core.appendProgram(createProposeProgram(params)) return txBuilder }, addSigner: (params: AddSignerParams) => { - programs.push(createAddSignerProgram(params)) + core.appendProgram(createAddSignerProgram(params)) return txBuilder }, attachMetadata: (params: AttachMetadataParams) => { - programs.push(createAttachMetadataProgram(params)) + core.appendProgram(createAttachMetadataProgram(params)) return txBuilder }, compose: (other: TransactionBuilder) => { - const otherPrograms = other.getPrograms() - if (otherPrograms.length > 0) { - programs.push(...otherPrograms) - } + core.appendPrograms(other.getPrograms()) return txBuilder }, + getPrograms: core.getPrograms, + buildEffect: (options?: BuildOptions) => core.buildEffect(options), + build: (options?: BuildOptions) => core.build(options), + buildEither: (options?: BuildOptions) => core.buildEither(options), + buildPartialEffect: (options?: BuildOptions) => core.buildPartialEffect(options), + buildPartial: (options?: BuildOptions) => core.buildPartial(options) + } - getPrograms: () => [...programs], + return txBuilder as TxBuilderResultType +} - buildEffect: (options?: BuildOptions) => { - return makeBuild(config, programs, options) - }, +const createCapabilityTxBuilderFromCore = (core: BuilderCore): TxBuilder => { + const current = (): TxBuilder => createCapabilityTxBuilderFromCore(core) + const next = (): TxBuilder => createCapabilityTxBuilderFromCore(core) + + function collectFrom( + params: CollectFromParams & { readonly redeemer: RedeemerArg } + ): TxBuilder + function collectFrom(params: CollectFromParams): TxBuilder + function collectFrom(params: CollectFromParams): TxBuilder | TxBuilder { + core.appendProgram(createCollectFromProgram(params)) + return params.redeemer === undefined ? current() : next() + } - build: (options?: BuildOptions) => { - const effect = makeBuild(config, programs, options) - return runEffectPromise( - options?.debug - ? effect.pipe(Effect.provide(Layer.merge(Logger.pretty, Logger.minimumLogLevel(LogLevel.Debug)))) - : effect - ) - }, - buildEither: (options?: BuildOptions) => { - const effect = makeBuild(config, programs, options).pipe(Effect.either) - return runEffectPromise( - options?.debug - ? effect.pipe(Effect.provide(Layer.merge(Logger.pretty, Logger.minimumLogLevel(LogLevel.Debug)))) - : effect - ) - }, + function mintAssets( + params: MintTokensParams & { readonly redeemer: RedeemerArg } + ): TxBuilder + function mintAssets(params: MintTokensParams): TxBuilder + function mintAssets(params: MintTokensParams): TxBuilder | TxBuilder { + core.appendProgram(createMintAssetsProgram(params)) + return params.redeemer === undefined ? current() : next() + } - // ============================================================================ - // Debug methods - Execute with fresh state, return partial transaction - // ============================================================================ + function withdraw( + params: WithdrawParams & { readonly redeemer: RedeemerArg } + ): TxBuilder + function withdraw(params: WithdrawParams): TxBuilder + function withdraw(params: WithdrawParams): TxBuilder | TxBuilder { + core.appendProgram(createWithdrawProgram(params)) + return params.redeemer === undefined ? current() : next() + } - buildPartialEffect: (options?: BuildOptions) => buildPartialEffectCore(config, programs, options), + function deregisterStake( + params: DeregisterStakeParams & { readonly redeemer: RedeemerArg } + ): TxBuilder + function deregisterStake(params: DeregisterStakeParams): TxBuilder + function deregisterStake( + params: DeregisterStakeParams + ): TxBuilder | TxBuilder { + core.appendProgram(createDeregisterStakeProgram(params)) + return params.redeemer === undefined ? current() : next() + } - buildPartial: (options?: BuildOptions) => runEffectPromise(buildPartialEffectCore(config, programs, options)) + return { + payToAddress: (params: PayToAddressParams) => { + core.appendProgram(createPayToAddressProgram(params)) + return current() + }, + attachScript: (params: { script: CoreScript.Script }) => { + core.appendProgram(createAttachScriptProgram(params.script)) + return current() + }, + readFrom: (params: ReadFromParams) => { + core.appendProgram(createReadFromProgram(params)) + return current() + }, + setValidity: (params: ValidityParams) => { + core.appendProgram(createSetValidityProgram(params)) + return current() + }, + addSigner: (params: AddSignerParams) => { + core.appendProgram(createAddSignerProgram(params)) + return current() + }, + sendAll: (params: SendAllParams) => { + core.appendProgram(createSendAllProgram(params)) + return current() + }, + attachMetadata: (params: AttachMetadataParams) => { + core.appendProgram(createAttachMetadataProgram(params)) + return current() + }, + registerStake: (params: RegisterStakeParams) => { + core.appendProgram(createRegisterStakeProgram(params)) + return current() + }, + registerPool: (params: RegisterPoolParams) => { + core.appendProgram(createRegisterPoolProgram(params)) + return current() + }, + retirePool: (params: RetirePoolParams) => { + core.appendProgram(createRetirePoolProgram(params)) + return current() + }, + registerDRep: (params: RegisterDRepParams) => { + core.appendProgram(createRegisterDRepProgram(params)) + return current() + }, + updateDRep: (params: UpdateDRepParams) => { + core.appendProgram(createUpdateDRepProgram(params)) + return current() + }, + deregisterDRep: (params: DeregisterDRepParams) => { + core.appendProgram(createDeregisterDRepProgram(params)) + return current() + }, + authCommitteeHot: (params: AuthCommitteeHotParams) => { + core.appendProgram(createAuthCommitteeHotProgram(params)) + return current() + }, + resignCommitteeCold: (params: ResignCommitteeColdParams) => { + core.appendProgram(createResignCommitteeColdProgram(params)) + return current() + }, + registerAndDelegateTo: (params: RegisterAndDelegateToParams) => { + core.appendProgram(createRegisterAndDelegateToProgram(params)) + return current() + }, + delegateTo: (params: DelegateToParams) => { + core.appendProgram(createDelegateToProgram(params)) + return current() + }, + delegateToPool: (params: DelegateToPoolParams) => { + core.appendProgram(createDelegateToPoolProgram(params)) + return current() + }, + delegateToDRep: (params: DelegateToDRepParams) => { + core.appendProgram(createDelegateToDRepProgram(params)) + return current() + }, + delegateToPoolAndDRep: (params: DelegateToPoolAndDRepParams) => { + core.appendProgram(createDelegateToPoolAndDRepProgram(params)) + return current() + }, + vote: (params: VoteParams) => { + core.appendProgram(createVoteProgram(params)) + return current() + }, + propose: (params: ProposeParams) => { + core.appendProgram(createProposeProgram(params)) + return current() + }, + collectFrom, + mintAssets, + withdraw, + deregisterStake, + compose: (other: TxBuilder): TxBuilder => { + core.appendPrograms(other.getPrograms()) + return next() + }, + getPrograms: core.getPrograms, + build: (...args: BuildArgs) => core.build>(args[0]), + buildEffect: (...args: BuildArgs) => core.buildEffect>(args[0]), + buildEither: (...args: BuildArgs) => core.buildEither>(args[0]) } +} - return txBuilder +/** + * Construct a capability-tracked transaction builder from immutable builder configuration. + * + * This constructor shares the same runtime builder core as `makeTxBuilder`, but returns the + * capability-aware `TxBuilder` facade used by `newTx(client)`. + * + * @since 2.1.0 + * @category constructors + */ +export const makeCapabilityTxBuilder = (config: TxBuilderConfig): TxBuilder => + createCapabilityTxBuilderFromCore(createBuilderCore(config)) + +/** + * Construct a TransactionBuilder instance from protocol configuration. + * + * The builder accumulates chainable method calls as deferred ProgramSteps. Calling build() or chain() + * creates fresh state (new Refs) and executes all accumulated programs sequentially, ensuring + * no state pollution between invocations. + * + * The return type is determined by the actual wallet provided using conditional types: + * - SigningTransactionBuilder: When wallet is SigningWallet or ApiWallet + * - ReadOnlyTransactionBuilder: When wallet is ReadOnlyWallet or undefined + * + * Wallet type narrowing happens at construction time based on the wallet's actual type. + * No call-site type narrowing or type guards needed. + * + * Wallet parameter is optional; if omitted, changeAddress and availableUtxos must be + * provided at build time via BuildOptions. + * + * @deprecated Use newTx(client) instead. newTx returns TxBuilder which enforces + * capability constraints at compile time via the client's pipe-composed capabilities. + * + * @since 2.0.0 + * @category constructors + * + */ +export function makeTxBuilder< + W extends WalletLike | undefined +>(config: Partial & { wallet?: W }): TxBuilderResultType +export function makeTxBuilder(config: TxBuilderConfig) { + return createLegacyBuilderFromCore(createBuilderCore(config)) } diff --git a/packages/evolution/src/sdk/builders/TxBuilderImpl.ts b/packages/evolution/src/sdk/builders/TxBuilderImpl.ts deleted file mode 100644 index a47af4c8..00000000 --- a/packages/evolution/src/sdk/builders/TxBuilderImpl.ts +++ /dev/null @@ -1,1706 +0,0 @@ -// Effect-TS imports -import { Effect, Ref } from "effect" -import type * as Array from "effect/Array" - -// Core imports -import * as CoreAddress from "../../Address.js" -import * as CoreAssets from "../../Assets/index.js" -import * as Bytes from "../../Bytes.js" -import type * as Certificate from "../../Certificate.js" -import * as CostModel from "../../CostModel.js" -import type * as PlutusData from "../../Data.js" -import type * as DatumOption from "../../DatumOption.js" -import * as Ed25519Signature from "../../Ed25519Signature.js" -import type * as KeyHash from "../../KeyHash.js" -import * as NativeScripts from "../../NativeScripts.js" -import type * as PlutusV1 from "../../PlutusV1.js" -import type * as PlutusV2 from "../../PlutusV2.js" -import type * as PlutusV3 from "../../PlutusV3.js" -import * as PolicyId from "../../PolicyId.js" -import * as Redeemer from "../../Redeemer.js" -import * as Redeemers from "../../Redeemers.js" -import type * as RewardAccount from "../../RewardAccount.js" -import * as CoreScript from "../../Script.js" -import * as ScriptDataHash from "../../ScriptDataHash.js" -import * as ScriptRef from "../../ScriptRef.js" -import * as Time from "../../Time/index.js" -import * as Transaction from "../../Transaction.js" -import * as TransactionBody from "../../TransactionBody.js" -import * as TransactionHash from "../../TransactionHash.js" -import * as TransactionInput from "../../TransactionInput.js" -import * as TransactionWitnessSet from "../../TransactionWitnessSet.js" -import * as TxOut from "../../TxOut.js" -import { hashAuxiliaryData, hashScriptData } from "../../utils/Hash.js" -import * as CoreUTxO from "../../UTxO.js" -import * as VKey from "../../VKey.js" -import * as Withdrawals from "../../Withdrawals.js" -// Internal imports -import { voterToKey } from "./phases/utils.js" -import type { UnfrackOptions } from "./TransactionBuilder.js" -import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "./TransactionBuilder.js" -import * as Unfrack from "./Unfrack.js" - -// ============================================================================ -// TransactionBuilder Effect Programs Implementation -// ============================================================================ - -/** - * This file contains the program creators that generate ProgramSteps. - * ProgramSteps are deferred Effects executed during build() with fresh state. - * - * Architecture: - * - Program creators return deferred Effects (ProgramSteps) - * - Programs access TxContext (single unified Context) containing config, state, and options - * - Programs are executed with fresh state on each build() call - * - No state mutation between builds - complete isolation - * - No prop drilling - everything accessible via single Context - */ - -// ============================================================================ -// Helper Functions - Address Utilities -// ============================================================================ - -/** - * Check if an address is a script address (payment credential is ScriptHash). - * Works with Core Address type. - * - * @since 2.0.0 - * @category helpers - */ -export const isScriptAddressCore = (address: CoreAddress.Address): boolean => { - return address.paymentCredential?._tag === "ScriptHash" -} - -/** - * Check if an address string is a script address (payment credential is ScriptHash). - * Parses the address to extract its structure and checks the payment credential type. - * - * @since 2.0.0 - * @category helpers - */ -export const isScriptAddress = (address: string): Effect.Effect => - Effect.gen(function* () { - // Parse address to structure - const addressStructure = yield* Effect.try({ - try: () => CoreAddress.fromBech32(address), - catch: (error) => - new TransactionBuilderError({ - message: `Failed to parse address: ${address}`, - cause: error - }) - }) - - // Check if payment credential is a script hash - return addressStructure.paymentCredential._tag === "ScriptHash" - }) - -/** - * Filter UTxOs to find those locked by scripts (script-locked UTxOs). - * - * @since 2.0.0 - * @category helpers - */ -export const filterScriptUtxos = ( - utxos: ReadonlyArray -): Effect.Effect, TransactionBuilderError> => - Effect.gen(function* () { - const scriptUtxos: Array = [] - - for (const utxo of utxos) { - // Core UTxO has address as Address class, check directly - if (isScriptAddressCore(utxo.address)) { - scriptUtxos.push(utxo) - } - } - - return scriptUtxos - }) - -// ============================================================================ -// Helper Functions - Asset Utilities -// ============================================================================ - -/** - * Calculate total assets from a set of UTxOs. - * - * @since 2.0.0 - * @category helpers - */ -export const calculateTotalAssets = (utxos: ReadonlyArray | Set): CoreAssets.Assets => { - const utxoArray = ( - globalThis.Array.isArray(utxos) ? utxos : globalThis.Array.from(utxos) - ) as ReadonlyArray - return utxoArray.reduce( - (total: CoreAssets.Assets, utxo: CoreUTxO.UTxO) => CoreAssets.merge(total, utxo.assets), - CoreAssets.zero - ) -} - -/** - * Calculate reference script fees using tiered pricing. - * - * Direct port of the Cardano ledger's `tierRefScriptFee` function. - * Each `sizeIncrement`-byte chunk is priced at `curTierPrice` per byte, - * then `curTierPrice *= multiplier` for the next chunk. Final result: `floor(total)`. - * - * @since 2.0.0 - * @category helpers - */ -export const tierRefScriptFee = (multiplier: number, sizeIncrement: number, baseFee: number, totalSize: number): bigint => { - let acc = 0 - let curTierPrice = baseFee - let remaining = totalSize - - while (remaining >= sizeIncrement) { - acc += sizeIncrement * curTierPrice - curTierPrice *= multiplier - remaining -= sizeIncrement - } - acc += remaining * curTierPrice - - return BigInt(Math.floor(acc)) -} - -/** - * Calculate reference script fees using tiered pricing. - * - * Matches the Cardano node's `tierRefScriptFee` from Conway ledger: - * - Stride: 25,600 bytes (hardcoded, becomes a protocol param post-Conway) - * - Multiplier: 1.2× per tier (hardcoded, becomes a protocol param post-Conway) - * - Base cost: `minFeeRefScriptCostPerByte` protocol parameter - * - * For each 25,600-byte chunk the price per byte increases by 1.2×. - * The final (partial) chunk is charged proportionally. Result is `floor(total)`. - * - * The Cardano node sums scriptRef sizes from both spent inputs and reference - * inputs (`txNonDistinctRefScriptsSize`), so callers must pass both. - * - * @param utxos - All UTxOs (spent inputs + reference inputs) to scan for scriptRefs - * @param costPerByte - The minFeeRefScriptCostPerByte protocol parameter - * @returns Total reference script fee in lovelace - * - * @since 2.0.0 - * @category helpers - */ -export const calculateReferenceScriptFee = ( - utxos: ReadonlyArray, - costPerByte: number -): Effect.Effect => - Effect.gen(function* () { - let totalScriptSize = 0 - - for (const utxo of utxos) { - if (utxo.scriptRef) { - const scriptBytes = CoreScript.toCBOR(utxo.scriptRef).length - totalScriptSize += scriptBytes - const scriptType = utxo.scriptRef._tag === "NativeScript" ? "Native" : "Plutus" - yield* Effect.logDebug(`[RefScriptFee] ${scriptType} script: ${scriptBytes} bytes`) - } - } - - if (totalScriptSize === 0) { - return 0n - } - - yield* Effect.logDebug(`[RefScriptFee] Total reference script size: ${totalScriptSize} bytes`) - - if (totalScriptSize > 200_000) { - // maxRefScriptSizePerTx from Conway ledger rules (CIP-0069 / CIP-0112) - return yield* Effect.fail( - new TransactionBuilderError({ - message: `Total reference script size (${totalScriptSize} bytes) exceeds maximum limit of 200,000 bytes` - }) - ) - } - - const fee = tierRefScriptFee(1.2, 25_600, costPerByte, totalScriptSize) - yield* Effect.logDebug(`[RefScriptFee] Tiered fee: ${fee} lovelace`) - - return fee - }) - - // ============================================================================ - // Helper Functions - Output Construction - // ============================================================================ - - .pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: `Failed to parse datum: ${error.message}`, - cause: error - }) - ) - ) - -/** - * Create a TransactionOutput from user-friendly parameters. - * Uses Core types directly. - * - * TransactionOutput represents an output being created in a transaction. - * - * @since 2.0.0 - * @category helpers - */ -export const makeTxOutput = (params: { - address: CoreAddress.Address - assets: CoreAssets.Assets - datum?: DatumOption.DatumOption - scriptRef?: CoreScript.Script -}): Effect.Effect => - Effect.gen(function* () { - // Convert Script to ScriptRef for CBOR encoding if provided - const scriptRefEncoded = params.scriptRef - ? new ScriptRef.ScriptRef({ bytes: CoreScript.toCBOR(params.scriptRef) }) - : undefined - - // Create Core TransactionOutput directly with core types - const output = new TxOut.TransactionOutput({ - address: params.address, - assets: params.assets, - datumOption: params.datum, - scriptRef: scriptRefEncoded - }) - - return output - }) - -/** - * Convert parameters to core TransactionOutput. - * This is an internal conversion function used during transaction assembly. - * Now uses Core types directly. - * - * @since 2.0.0 - * @category helpers - * @internal - */ -export const txOutputToTransactionOutput = (params: { - address: CoreAddress.Address - assets: CoreAssets.Assets - datum?: DatumOption.DatumOption - scriptRef?: CoreScript.Script -}): Effect.Effect => - Effect.gen(function* () { - // Convert Script to ScriptRef for CBOR encoding if provided - const scriptRefEncoded = params.scriptRef - ? new ScriptRef.ScriptRef({ bytes: CoreScript.toCBOR(params.scriptRef) }) - : undefined - - // Create TransactionOutput directly with core types - const output = new TxOut.TransactionOutput({ - address: params.address, - assets: params.assets, - datumOption: params.datum, - scriptRef: scriptRefEncoded - }) - - return output - }) - -/** - * Merge additional assets into an existing UTxO. - * Creates a new UTxO with combined assets from the original UTxO and additional assets. - * - * Use case: Draining wallet by merging leftover into an existing payment output. - * - * @since 2.0.0 - * @category helpers - */ -export const mergeAssetsIntoUTxO = ( - utxo: CoreUTxO.UTxO, - additionalAssets: CoreAssets.Assets -): Effect.Effect => - Effect.gen(function* () { - // Merge assets using Core Assets helper - const mergedAssets = CoreAssets.merge(utxo.assets, additionalAssets) - // Create new UTxO with merged assets - return new CoreUTxO.UTxO({ - transactionId: utxo.transactionId, - index: utxo.index, - address: utxo.address, - assets: mergedAssets, - datumOption: utxo.datumOption, - scriptRef: utxo.scriptRef - }) - }) - -/** - * Merge additional assets into an existing TransactionOutput. - * Creates a new output with combined assets from the original output and leftover assets. - * - * Use case: Draining wallet by merging leftover into an existing payment output. - * - * @since 2.0.0 - * @category helpers - */ -export const mergeAssetsIntoOutput = ( - output: TxOut.TransactionOutput, - additionalAssets: CoreAssets.Assets -): Effect.Effect => - Effect.gen(function* () { - // Merge assets using Core Assets helper - const mergedAssets = CoreAssets.merge(output.assets, additionalAssets) - - // Create new output with merged assets, preserving optional fields - const newOutput = new TxOut.TransactionOutput({ - address: output.address, - assets: mergedAssets, - datumOption: output.datumOption, - scriptRef: output.scriptRef - }) - return newOutput - }) - -// ============================================================================ -// Transaction Assembly -// ============================================================================ - -/** - * Convert an array of UTxOs to an array of TransactionInputs. - * Inputs are sorted by txHash then outputIndex for deterministic ordering. - * Uses Core UTxO types directly. - * - * @since 2.0.0 - * @category assembly - */ -export const buildTransactionInputs = ( - utxos: ReadonlyArray -): Effect.Effect, never> => - Effect.gen(function* () { - // Convert each Core UTxO to TransactionInput - const inputs: Array = [] - - for (const utxo of utxos) { - // Create TransactionInput directly from Core UTxO fields - const input = new TransactionInput.TransactionInput({ - transactionId: utxo.transactionId, - index: utxo.index - }) - - inputs.push(input) - } - - // Sort inputs for deterministic ordering: - // First by transaction hash, then by output index - inputs.sort((a, b) => { - // Compare transaction hashes (byte arrays) - const hashA = a.transactionId.hash - const hashB = b.transactionId.hash - - for (let i = 0; i < hashA.length; i++) { - if (hashA[i] !== hashB[i]) { - return hashA[i] - hashB[i] - } - } - - // If hashes are equal, compare by index - return Number(a.index - b.index) - }) - - return inputs - }) - -/** - * Assemble a Transaction from inputs, outputs, and calculated fee. - * Creates TransactionBody with all required fields. - * - * Uses Core TransactionOutput directly. - * - * This is minimal assembly with accurate fee: - * - Build witness set with redeemers and signatures (Step 4 - future) - * - Run script evaluation to fill ExUnits (Step 5 - future) - * - Add change output (Step 6 - future) - * - * @since 2.0.0 - * @category assembly - */ -export const assembleTransaction = ( - inputs: ReadonlyArray, - outputs: ReadonlyArray, - fee: bigint -): Effect.Effect => - Effect.gen(function* () { - // Get state ref to access scripts and redeemers - const stateRef = yield* TxContext - const state = yield* Ref.get(stateRef) - - yield* Effect.logDebug(`[Assembly] Building transaction with ${inputs.length} inputs, ${outputs.length} outputs`) - yield* Effect.logDebug(`[Assembly] Reference inputs in state: ${state.referenceInputs.length}`) - yield* Effect.logDebug(`[Assembly] Scripts in state: ${state.scripts.size}`) - yield* Effect.logDebug(`[Assembly] Redeemers in state: ${state.redeemers.size}`) - - // Outputs are already Core TransactionOutputs - const transactionOutputs = outputs as Array - - // Build collateral inputs if present - let collateralInputs: Array.NonEmptyReadonlyArray | undefined - let collateralReturn: TxOut.TransactionOutput | undefined - let totalCollateral: bigint | undefined - - if (state.collateral) { - yield* Effect.logDebug( - `[Assembly] Adding collateral: ${state.collateral.inputs.length} inputs, ` + - `total ${state.collateral.totalAmount} lovelace` - ) - - // Collateral phase guarantees at least one input for script transactions - collateralInputs = (yield* buildTransactionInputs( - state.collateral.inputs - )) as Array.NonEmptyReadonlyArray - totalCollateral = state.collateral.totalAmount - - // Collateral return is already a Core TransactionOutput - if (state.collateral.returnOutput) { - yield* Effect.logDebug( - `[Assembly] Collateral return lovelace: ${state.collateral.returnOutput.assets.lovelace}` - ) - collateralReturn = state.collateral.returnOutput - } - } - - // Convert reference inputs from UTxOs to TransactionInputs (only if there are any) - let referenceInputs: - | readonly [TransactionInput.TransactionInput, ...Array] - | undefined - if (state.referenceInputs.length > 0) { - const refInputs = yield* buildTransactionInputs(state.referenceInputs) - referenceInputs = refInputs as readonly [ - TransactionInput.TransactionInput, - ...Array - ] - } - - // Populate witness set with scripts from state - const plutusV1Scripts: Array = [] - const plutusV2Scripts: Array = [] - const plutusV3Scripts: Array = [] - const nativeScripts: Array = [] // TODO: Add native script type - - // Group scripts by type - for (const [scriptHash, coreScript] of state.scripts) { - yield* Effect.logDebug(`[Assembly] Processing script with hash: ${scriptHash}, type: ${coreScript._tag}`) - - switch (coreScript._tag) { - case "PlutusV1": - plutusV1Scripts.push(coreScript) - break - case "PlutusV2": - plutusV2Scripts.push(coreScript) - break - case "PlutusV3": - plutusV3Scripts.push(coreScript) - break - case "NativeScript": - nativeScripts.push(coreScript) - break - } - } - - // Build redeemers array from state FIRST (needed for scriptDataHash) - const redeemers: Array = [] - - // Create a mapping from UTxO reference (txHash#outputIndex) to input index - const inputIndexMap = new Map() - for (let i = 0; i < inputs.length; i++) { - const input = inputs[i]! - const txHashHex = TransactionHash.toHex(input.transactionId) - const key = `${txHashHex}#${input.index}` - yield* Effect.logDebug(`[Assembly] Input ${i}: ${key}`) - inputIndexMap.set(key, i) - } - - yield* Effect.logDebug(`[Assembly] Input index map has ${inputIndexMap.size} entries`) - yield* Effect.logDebug(`[Assembly] Redeemer map keys: ${globalThis.Array.from(state.redeemers.keys()).join(", ")}`) - - // Create a mapping from PolicyId hex to mint index (sorted order) - const mintIndexMap = new Map() - if (state.mint && state.mint.map.size > 0) { - // Get sorted policy IDs (Cardano redeemer indices require sorted order) - const sortedPolicyIds = globalThis.Array.from(state.mint.map.keys()) - .map((pid) => PolicyId.toHex(pid)) - .sort() - - for (let i = 0; i < sortedPolicyIds.length; i++) { - mintIndexMap.set(sortedPolicyIds[i]!, i) - yield* Effect.logDebug(`[Assembly] Mint policy ${i}: ${sortedPolicyIds[i]}`) - } - } - - // Build redeemers with correct indices - // For cert/reward redeemers, we need to find their index in the certificates/withdrawals arrays - // Keys are stored as `cert:{hex}` and `reward:{hex}` in state.redeemers - - for (const [key, redeemerData] of state.redeemers) { - yield* Effect.logDebug(`[Assembly] Processing redeemer for key: ${key}, tag: ${redeemerData.tag}`) - - let redeemerIndex: number | undefined - - if (redeemerData.tag === "mint") { - // For mint redeemers, look up in mint index map - redeemerIndex = mintIndexMap.get(key) - if (redeemerIndex === undefined) { - yield* Effect.logWarning(`[Assembly] Could not find mint index for policy: ${key}`) - continue - } - } else if (redeemerData.tag === "cert") { - // For cert redeemers, find matching certificate by credential hash - // Key format: `cert:{credentialHex}` - const credentialHex = key.slice(5) // Remove "cert:" prefix - for (let i = 0; i < state.certificates.length; i++) { - const cert = state.certificates[i]! - // Check stakeCredential (for stake-related certs) - if ("stakeCredential" in cert && cert.stakeCredential) { - const certCredHex = Bytes.toHex((cert.stakeCredential as { hash: Uint8Array }).hash) - if (certCredHex === credentialHex) { - redeemerIndex = i - break - } - } - // Check drepCredential (for DRep-related certs: RegDrepCert, UnregDrepCert, UpdateDrepCert) - if ("drepCredential" in cert && cert.drepCredential) { - const certCredHex = Bytes.toHex((cert.drepCredential as { hash: Uint8Array }).hash) - if (certCredHex === credentialHex) { - redeemerIndex = i - break - } - } - } - if (redeemerIndex === undefined) { - yield* Effect.logWarning(`[Assembly] Could not find cert index for key: ${key}`) - continue - } - } else if (redeemerData.tag === "reward") { - // For reward redeemers, find matching withdrawal by credential hash (sorted order) - // Key format: `reward:{credentialHex}` - const credentialHex = key.slice(7) // Remove "reward:" prefix - // Withdrawals must be in sorted order for redeemer indices - const sortedWithdrawals = globalThis.Array.from(state.withdrawals.entries()).sort((a, b) => { - const aHex = Bytes.toHex(a[0].stakeCredential.hash) - const bHex = Bytes.toHex(b[0].stakeCredential.hash) - return aHex.localeCompare(bHex) - }) - for (let i = 0; i < sortedWithdrawals.length; i++) { - const [rewardAccount] = sortedWithdrawals[i]! - const rewardCredHex = Bytes.toHex(rewardAccount.stakeCredential.hash) - if (rewardCredHex === credentialHex) { - redeemerIndex = i - break - } - } - if (redeemerIndex === undefined) { - yield* Effect.logWarning(`[Assembly] Could not find withdrawal index for key: ${key}`) - continue - } - } else if (redeemerData.tag === "vote") { - // For vote redeemers, find matching voter in votingProcedures (sorted order) - // Key format: `drep:{credentialHex}` | `cc:{credentialHex}` | `pool:{poolKeyHashHex}` - - if (!state.votingProcedures) { - yield* Effect.logWarning(`[Assembly] Vote redeemer found but no votingProcedures in state`) - continue - } - - // Build sorted voter keys from votingProcedures using shared utility - const sortedVoterKeys: Array = [] - for (const voter of state.votingProcedures.procedures.keys()) { - sortedVoterKeys.push(voterToKey(voter)) - } - - // Sort keys lexicographically (as per Cardano ledger rules) - sortedVoterKeys.sort() - - // Find the index of our voter key - for (let i = 0; i < sortedVoterKeys.length; i++) { - if (sortedVoterKeys[i] === key) { - redeemerIndex = i - break - } - } - if (redeemerIndex === undefined) { - yield* Effect.logWarning(`[Assembly] Could not find voter index for key: ${key}`) - continue - } - } else { - // For spend redeemers, look up in input index map - redeemerIndex = inputIndexMap.get(key) - if (redeemerIndex === undefined) { - yield* Effect.logWarning(`[Assembly] Could not find input index for redeemer key: ${key}`) - continue - } - } - - yield* Effect.logDebug( - `[Assembly] Redeemer exUnits before creating: mem=${redeemerData.exUnits?.mem ?? 0n}, steps=${redeemerData.exUnits?.steps ?? 0n}` - ) - - // Create proper Redeemer object - const redeemer = new Redeemer.Redeemer({ - tag: redeemerData.tag, // "spend", "mint", "cert", or "reward" - index: BigInt(redeemerIndex), // Use actual redeemer index - data: redeemerData.data, - exUnits: redeemerData.exUnits - ? new Redeemer.ExUnits({ mem: redeemerData.exUnits.mem, steps: redeemerData.exUnits.steps }) - : new Redeemer.ExUnits({ mem: 0n, steps: 0n }) // will be updated by script evaluation - }) - - yield* Effect.logDebug( - `[Assembly] Created redeemer: tag=${redeemer.tag}, index=${redeemer.index}, exUnits=[${redeemer.exUnits.mem}, ${redeemer.exUnits.steps}]` - ) - - redeemers.push(redeemer) - } - - // Extract plutus data (datums) from selected UTxOs - // NOTE: Only datum hashes need to be resolved in the witness set's plutusData field. - // Inline datums (Babbage era feature) are already embedded in the UTxO output - // and should NOT be included in the witness set - doing so causes "extraneous datums" error. - const plutusDataArray: Array = [] - for (const utxo of state.selectedUtxos) { - if (utxo.datumOption?._tag === "DatumHash") { - // For datum hash, we need to resolve and include the actual datum - // TODO: Implement datum resolution from provider or state - yield* Effect.logDebug(`[Assembly] Found datum hash UTxO (resolution not yet implemented)`) - } - // Inline datums (InlineDatum) are NOT added to plutusData - they're already in the UTxO - } - - // Compute scriptDataHash if there are Plutus scripts (redeemers present) - let scriptDataHash: ReturnType | undefined - let redeemersConcrete: Redeemers.RedeemerMap | undefined - if (redeemers.length > 0) { - // Get config to access provider for full protocol parameters - const config = yield* TxBuilderConfigTag - - if (!config.provider) { - throw new TransactionBuilderError({ - message: - "Script transactions require a provider to fetch full protocol parameters for scriptDataHash calculation", - cause: { redeemerCount: redeemers.length } - }) - } - - // Fetch full protocol params from provider (includes cost models) - const fullProtocolParams = yield* config.provider.Effect.getProtocolParameters().pipe( - Effect.mapError( - (providerError) => - new TransactionBuilderError({ - message: `Failed to fetch full protocol parameters for scriptDataHash calculation: ${providerError.message}`, - cause: providerError - }) - ) - ) - - // Only include cost models for Plutus versions actually used in the transaction - // The scriptDataHash must use the same languages as the node will compute - // Check: 1) witness set scripts (attachScript), 2) reference inputs (readFrom), - // 3) spent UTxO scriptRefs (inline scripts on inputs being consumed) - let hasPlutusV1 = plutusV1Scripts.length > 0 - let hasPlutusV2 = plutusV2Scripts.length > 0 - let hasPlutusV3 = plutusV3Scripts.length > 0 - - // Also check reference inputs for Plutus scripts - for (const refUtxo of state.referenceInputs) { - if (refUtxo.scriptRef) { - switch (refUtxo.scriptRef._tag) { - case "PlutusV1": - hasPlutusV1 = true - break - case "PlutusV2": - hasPlutusV2 = true - break - case "PlutusV3": - hasPlutusV3 = true - break - } - } - } - - // Also check spent UTxOs for inline scriptRef (scripts embedded in the UTxO itself) - // When a script-locked UTxO carries its own script as scriptRef, the node uses that - // script for validation. The SDK must include the corresponding language's cost model. - for (const utxo of state.selectedUtxos) { - if (utxo.scriptRef) { - switch (utxo.scriptRef._tag) { - case "PlutusV1": - hasPlutusV1 = true - break - case "PlutusV2": - hasPlutusV2 = true - break - case "PlutusV3": - hasPlutusV3 = true - break - } - } - } - - const plutusV1Costs = hasPlutusV1 - ? Object.values(fullProtocolParams.costModels.PlutusV1).map((v) => BigInt(v)) - : [] // Empty array = not included in language_views - const plutusV2Costs = hasPlutusV2 - ? Object.values(fullProtocolParams.costModels.PlutusV2).map((v) => BigInt(v)) - : [] // Empty array = not included in language_views - const plutusV3Costs = hasPlutusV3 - ? Object.values(fullProtocolParams.costModels.PlutusV3).map((v) => BigInt(v)) - : [] // Empty array = not included in language_views - - yield* Effect.logDebug(`[Assembly] Cost models included: V1=${hasPlutusV1}, V2=${hasPlutusV2}, V3=${hasPlutusV3}`) - - const costModels = new CostModel.CostModels({ - PlutusV1: new CostModel.CostModel({ costs: plutusV1Costs }), - PlutusV2: new CostModel.CostModel({ costs: plutusV2Costs }), - PlutusV3: new CostModel.CostModel({ costs: plutusV3Costs }) - }) - - // Compute the hash of script data (redeemers + optional datums + cost models) - // Use the same concrete Redeemers type that goes into the witness set - redeemersConcrete = Redeemers.makeRedeemerMap(redeemers) - scriptDataHash = hashScriptData( - redeemersConcrete, - costModels, - plutusDataArray.length > 0 ? plutusDataArray : undefined - ) - yield* Effect.logDebug( - `[Assembly] Computed scriptDataHash: ${scriptDataHash.hash.toString()}` - ) - } - - yield* Effect.logDebug(`[Assembly] WitnessSet populated:`) - yield* Effect.logDebug(` - PlutusV1 scripts: ${plutusV1Scripts.length}`) - yield* Effect.logDebug(` - PlutusV2 scripts: ${plutusV2Scripts.length}`) - yield* Effect.logDebug(` - PlutusV3 scripts: ${plutusV3Scripts.length}`) - yield* Effect.logDebug(` - Redeemers: ${redeemers.length}`) - yield* Effect.logDebug(` - Plutus data: ${plutusDataArray.length}`) - - // Create TransactionBody with calculated fee and scriptDataHash - // Build certificates array (NonEmptyArray or undefined) - const certificates = - state.certificates.length > 0 - ? (state.certificates as [Certificate.Certificate, ...Array]) - : undefined - - // Build withdrawals (Withdrawals object or undefined) - const withdrawals = - state.withdrawals.size > 0 - ? new Withdrawals.Withdrawals({ withdrawals: state.withdrawals as Map }) - : undefined - - // Convert validity interval from Unix time to slots - // Use resolved slot config from BuildOptionsTag (respects BuildOptions > TxBuilderConfig > network default priority) - const buildOptions = yield* BuildOptionsTag - const slotConfig = buildOptions.slotConfig! - - let ttl: bigint | undefined - let validityIntervalStart: bigint | undefined - - if (state.validity?.to !== undefined) { - ttl = Time.unixTimeToSlot(state.validity.to, slotConfig) - yield* Effect.logDebug(`[Assembly] Validity TTL: ${ttl} (from unix ${state.validity.to})`) - } - if (state.validity?.from !== undefined) { - validityIntervalStart = Time.unixTimeToSlot(state.validity.from, slotConfig) - yield* Effect.logDebug(`[Assembly] Validity start: ${validityIntervalStart} (from unix ${state.validity.from})`) - } - - // Build required signers (NonEmptyArray or undefined) - const requiredSigners = - state.requiredSigners.length > 0 - ? (state.requiredSigners as [KeyHash.KeyHash, ...Array]) - : undefined - - if (requiredSigners) { - yield* Effect.logDebug(`[Assembly] Required signers: ${requiredSigners.length}`) - } - - // Compute auxiliary data hash if auxiliary data is present - let auxiliaryDataHash: ReturnType | undefined - if (state.auxiliaryData) { - auxiliaryDataHash = hashAuxiliaryData(state.auxiliaryData) - yield* Effect.logDebug(`[Assembly] Computed auxiliaryDataHash: ${auxiliaryDataHash.toString()}`) - } - - const body = new TransactionBody.TransactionBody({ - inputs: inputs as Array, - outputs: transactionOutputs, - fee, // Now using actual calculated fee, not placeholder - ttl, // Transaction expiration slot - validityIntervalStart, // Transaction valid-from slot - collateralInputs, // Collateral inputs from Collateral phase - collateralReturn, // Collateral return output from Collateral phase - totalCollateral, // Total collateral amount from Collateral phase - referenceInputs, // Reference inputs for reading on-chain data (undefined if none) - mint: state.mint && state.mint.map.size > 0 ? state.mint : undefined, // Mint field from minting operations - scriptDataHash, // Hash of redeemers + datums + cost models (required for Plutus scripts) - auxiliaryDataHash, // Hash of auxiliary data (required when metadata is present) - certificates, // Certificates for staking operations - withdrawals, // Withdrawals for claiming staking rewards - requiredSigners, // Extra signers required for script validation - votingProcedures: state.votingProcedures, // Voting procedures for governance voting - proposalProcedures: state.proposalProcedures // Proposal procedures for governance proposals - }) - - // Create witness set with scripts and redeemers - const witnessSet = new TransactionWitnessSet.TransactionWitnessSet({ - vkeyWitnesses: [], - nativeScripts, - bootstrapWitnesses: [], - plutusV1Scripts, - plutusData: plutusDataArray, - redeemers: redeemers.length > 0 ? redeemersConcrete : undefined, - plutusV2Scripts, - plutusV3Scripts - }) - - // Create Transaction - const transaction = new Transaction.Transaction({ - body, - witnessSet, - isValid: true, // Assume valid until script evaluation proves otherwise - auxiliaryData: state.auxiliaryData ?? null - }) - - return transaction - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: `Failed to assemble transaction: ${error.message}`, - cause: error - }) - ) - ) - -// ============================================================================ -// Fee Calculation -// ============================================================================ - -/** - * Calculate the size of a transaction in bytes for fee estimation. - * Uses CBOR serialization to get accurate size. - * - * @since 2.0.0 - * @category fee-calculation - */ -export const calculateTransactionSize = ( - transaction: Transaction.Transaction -): Effect.Effect => - Effect.gen(function* () { - // Serialize transaction to CBOR bytes using sync function - const cborBytes = yield* Effect.try({ - try: () => Transaction.toCBORBytes(transaction), - catch: (error) => - new TransactionBuilderError({ - message: "Failed to encode transaction to CBOR", - cause: error - }) - }) - - return cborBytes.length - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: `Failed to calculate transaction size: ${error.message}`, - cause: error - }) - ) - ) - -/** - * Calculate minimum transaction fee based on protocol parameters. - * - * Formula: minFee = txSizeInBytes × minFeeCoefficient + minFeeConstant - * - * @since 2.0.0 - * @category fee-calculation - */ -export const calculateMinimumFee = ( - transactionSizeBytes: number, - protocolParams: { - minFeeCoefficient: bigint // minFeeA - minFeeConstant: bigint // minFeeB - } -): bigint => { - const { minFeeCoefficient, minFeeConstant } = protocolParams - - return BigInt(transactionSizeBytes) * minFeeCoefficient + minFeeConstant -} - -/** - * Extract payment key hash from a Cardano address. - * Returns null if address has script credential or no payment credential. - * - * @since 2.0.0 - * @category fee-calculation - * @internal - */ -export const extractPaymentKeyHash = (address: string): Effect.Effect => - Effect.gen(function* () { - const addressStructure = yield* Effect.try({ - try: () => CoreAddress.fromBech32(address), - catch: (error) => - new TransactionBuilderError({ - message: `Failed to parse address ${address}`, - cause: error - }) - }) - - // Check if payment credential is a KeyHash - if (addressStructure.paymentCredential?._tag === "KeyHash" && addressStructure.paymentCredential.hash) { - return addressStructure.paymentCredential.hash - } - - return null - }) - -/** - * Extract payment key hash from a Core Address. - * Returns null if address has script credential or no payment credential. - * - * @since 2.0.0 - * @category fee-calculation - * @internal - */ -const extractPaymentKeyHashFromCore = (address: CoreAddress.Address): Uint8Array | null => { - // Check if payment credential is a KeyHash - if (address.paymentCredential._tag === "KeyHash" && address.paymentCredential.hash) { - return address.paymentCredential.hash - } - return null -} - -/** - * Build a fake VKeyWitness for fee estimation. - * Creates a witness with 32-byte vkey and 64-byte signature (96 bytes total). - * This matches CML's approach for accurate witness size calculation. - * - * @since 2.0.0 - * @category fee-calculation - * @internal - */ -const buildFakeVKeyWitness = ( - keyHash: Uint8Array -): Effect.Effect => - Effect.gen(function* () { - // Pad key hash to 32 bytes for vkey (Ed25519 public key size) - const vkeyBytes = new Uint8Array(32) - vkeyBytes.set(keyHash.slice(0, Math.min(keyHash.length, 32))) - - // Create 64-byte dummy signature (Ed25519 signature size) - const signatureBytes = new Uint8Array(64) - - const vkey = yield* Effect.try({ - try: () => new VKey.VKey({ bytes: vkeyBytes }), - catch: (error) => - new TransactionBuilderError({ - message: "Failed to create fake VKey", - cause: error - }) - }) - - const signature = yield* Effect.try({ - try: () => new Ed25519Signature.Ed25519Signature({ bytes: signatureBytes }), - catch: (error) => - new TransactionBuilderError({ - message: "Failed to create fake signature", - cause: error - }) - }) - - return new TransactionWitnessSet.VKeyWitness({ - vkey, - signature - }) - }) - -/** - * Build a fake witness set for fee estimation from transaction inputs. - * Extracts unique payment key hashes from input addresses and creates - * fake witnesses to accurately estimate witness set size in CBOR. - * Includes any attached scripts from builder state for accurate size estimation. - * - * @since 2.0.0 - * @category fee-calculation - */ -export const buildFakeWitnessSet = ( - inputUtxos: ReadonlyArray -): Effect.Effect => - Effect.gen(function* () { - const stateRef = yield* TxContext - const state = yield* Ref.get(stateRef) - - // Extract unique key hashes from input addresses (Core Address) - const keyHashesSet = new Set() - const keyHashes: Array = [] - - for (const utxo of inputUtxos) { - const keyHash = extractPaymentKeyHashFromCore(utxo.address) - if (keyHash) { - const keyHashHex = Bytes.toHex(keyHash) - if (!keyHashesSet.has(keyHashHex)) { - keyHashesSet.add(keyHashHex) - keyHashes.push(keyHash) - } - } - } - - // Collect attached scripts from state and count required signers for native scripts - const nativeScripts: Array = [] - const plutusV1Scripts: Array = [] - const plutusV2Scripts: Array = [] - const plutusV3Scripts: Array = [] - - // Helper to add dummy witnesses for native script required signers - const addNativeScriptWitnesses = (script: NativeScripts.NativeScript) => { - const requiredSigners = NativeScripts.countRequiredSigners(script.script) - for (let i = 0; i < requiredSigners; i++) { - const dummyKeyHash = new Uint8Array(28) - // Fill with unique pattern: 0xFF prefix + counter to distinguish from real keys - dummyKeyHash[0] = 0xff - dummyKeyHash[1] = (keyHashesSet.size + i) & 0xff - const dummyHashHex = Bytes.toHex(dummyKeyHash) - - // Only add if not already in the set - if (!keyHashesSet.has(dummyHashHex)) { - keyHashesSet.add(dummyHashHex) - keyHashes.push(dummyKeyHash) - } - } - return requiredSigners - } - - for (const script of state.scripts.values()) { - switch (script._tag) { - case "NativeScript": { - nativeScripts.push(script) - // Count required signers for this native script and add fake witnesses - const requiredSigners = addNativeScriptWitnesses(script) - yield* Effect.logDebug(`[buildFakeWitnessSet] Native script requires ${requiredSigners} signers`) - break - } - case "PlutusV1": - plutusV1Scripts.push(script) - break - case "PlutusV2": - plutusV2Scripts.push(script) - break - case "PlutusV3": - plutusV3Scripts.push(script) - break - } - } - - // Also count required signers from reference scripts (scripts in referenceInputs) - for (const refUtxo of state.referenceInputs) { - if (refUtxo.scriptRef && refUtxo.scriptRef._tag === "NativeScript") { - const requiredSigners = addNativeScriptWitnesses(refUtxo.scriptRef) - yield* Effect.logDebug(`[buildFakeWitnessSet] Reference native script requires ${requiredSigners} signers`) - } - } - - // Build fake witnesses for each unique key hash (inputs + native script signers) - const vkeyWitnesses: Array = [] - for (const keyHash of keyHashes) { - const witness = yield* buildFakeVKeyWitness(keyHash) - vkeyWitnesses.push(witness) - } - - // Add fake witnesses for certificates that require key signatures - // Certificates like RegCert, UnregCert, StakeDelegation etc. require the stake key to sign - for (const cert of state.certificates) { - let credentialHash: Uint8Array | undefined - - // Extract credential from certificate types that require signing - if ("stakeCredential" in cert && cert.stakeCredential._tag === "KeyHash") { - credentialHash = cert.stakeCredential.hash - } - - if (credentialHash) { - const hashHex = Bytes.toHex(credentialHash) - if (!keyHashesSet.has(hashHex)) { - keyHashesSet.add(hashHex) - const witness = yield* buildFakeVKeyWitness(credentialHash) - vkeyWitnesses.push(witness) - } - } - } - - // Add fake witnesses for withdrawals that require key signatures - for (const [rewardAccount, _amount] of state.withdrawals) { - // RewardAccount has stakeCredential property - const credential = rewardAccount.stakeCredential - if (credential._tag === "KeyHash") { - const hashHex = Bytes.toHex(credential.hash) - if (!keyHashesSet.has(hashHex)) { - keyHashesSet.add(hashHex) - const witness = yield* buildFakeVKeyWitness(credential.hash) - vkeyWitnesses.push(witness) - } - } - } - - // Add fake witnesses for required signers (from addSigner operation) - // These key hashes are explicitly required to sign the transaction - for (const keyHash of state.requiredSigners) { - const hashHex = Bytes.toHex(keyHash.hash) - if (!keyHashesSet.has(hashHex)) { - keyHashesSet.add(hashHex) - const witness = yield* buildFakeVKeyWitness(keyHash.hash) - vkeyWitnesses.push(witness) - } - } - - // Build fake redeemers from state.redeemers for accurate size estimation - // Redeemers contribute to transaction size and must be included in fee calculation - const fakeRedeemers: Array = [] - let fakeIndex = 0n - for (const [_key, redeemerData] of state.redeemers) { - // Use placeholder exUnits if not yet evaluated (will be updated after UPLC evaluation) - const exUnits = redeemerData.exUnits ?? { mem: 0n, steps: 0n } - - // Use unique placeholder indices — actual indices will be computed in assembly. - // For fee calculation, we just need accurate CBOR size estimation. - fakeRedeemers.push( - new Redeemer.Redeemer({ - tag: redeemerData.tag, - index: fakeIndex++, // Unique placeholder, will be set correctly in assembly - data: redeemerData.data, - exUnits: new Redeemer.ExUnits({ mem: exUnits.mem, steps: exUnits.steps }) - }) - ) - } - - return new TransactionWitnessSet.TransactionWitnessSet({ - vkeyWitnesses, - nativeScripts, - bootstrapWitnesses: [], - plutusV1Scripts, - plutusData: [], - redeemers: fakeRedeemers.length > 0 ? Redeemers.makeRedeemerMap(fakeRedeemers) : undefined, - plutusV2Scripts, - plutusV3Scripts - }) - }) - -/** - * Calculate transaction fee iteratively until stable. - * - * Algorithm: - * 1. Build fake witness set from input UTxOs for accurate size estimation - * 2. Build transaction with fee = 0 - * 3. Calculate size and fee - * 4. Rebuild transaction with calculated fee - * 5. If size changed, recalculate (usually converges in 1-2 iterations) - * - * @since 2.0.0 - * @category fee-calculation - */ -export const calculateFeeIteratively = ( - inputUtxos: ReadonlyArray, - inputs: ReadonlyArray, - outputs: ReadonlyArray, - redeemers: Map< - string, - { - readonly tag: "spend" | "mint" | "cert" | "reward" | "vote" - readonly data: PlutusData.Data - readonly exUnits?: { readonly mem: bigint; readonly steps: bigint } - } - >, - protocolParams: { - minFeeCoefficient: bigint - minFeeConstant: bigint - priceMem?: number - priceStep?: number - } -): Effect.Effect => - Effect.gen(function* () { - // Get state to access mint field and collateral - const stateRef = yield* TxContext - const state = yield* Ref.get(stateRef) - - // Include collateral UTxOs in witness estimation - they require VKey witnesses too! - const allUtxosForWitnesses = state.collateral ? [...inputUtxos, ...state.collateral.inputs] : inputUtxos - - // Build fake witness set once for accurate size estimation - const fakeWitnessSet = yield* buildFakeWitnessSet(allUtxosForWitnesses) - - // Outputs are already Core TransactionOutputs - const transactionOutputs = outputs as Array - - // Get mint field from state (if present) - const mint = state.mint && state.mint.map.size > 0 ? state.mint : undefined - - // Get collateral from state (for script transactions) - let collateralInputs: Array.NonEmptyReadonlyArray | undefined - let collateralReturn: TxOut.TransactionOutput | undefined - let totalCollateral: bigint | undefined - if (state.collateral) { - const builtCollateralInputs = yield* buildTransactionInputs(state.collateral.inputs) - // Only set collateralInputs if there's at least one input - if (builtCollateralInputs.length > 0) { - collateralInputs = builtCollateralInputs as Array.NonEmptyReadonlyArray - } - collateralReturn = state.collateral.returnOutput - totalCollateral = state.collateral.totalAmount - } - - // Check if Plutus scripts are present (need scriptDataHash for accurate size) - // Must check: witness set scripts, redeemers (covers scriptRef spending), and reference input scripts - const hasPlutusScripts = - (fakeWitnessSet.plutusV1Scripts && fakeWitnessSet.plutusV1Scripts.length > 0) || - (fakeWitnessSet.plutusV2Scripts && fakeWitnessSet.plutusV2Scripts.length > 0) || - (fakeWitnessSet.plutusV3Scripts && fakeWitnessSet.plutusV3Scripts.length > 0) || - state.redeemers.size > 0 - - // Create placeholder scriptDataHash if Plutus scripts are present - // This is needed for accurate size estimation (32 bytes + CBOR overhead) - const placeholderScriptDataHash = hasPlutusScripts - ? new ScriptDataHash.ScriptDataHash({ - hash: new Uint8Array(32) // Placeholder hash for size calculation - }) - : undefined - - // Create placeholder auxiliaryDataHash if auxiliary data is present - // This is needed for accurate size estimation (32 bytes + CBOR overhead) - const placeholderAuxiliaryDataHash = state.auxiliaryData ? hashAuxiliaryData(state.auxiliaryData) : undefined - - let currentFee = 0n - let previousSize = 0 - let previousFee = 0n - let iterations = 0 - const maxIterations = 10 // Increase to ensure convergence - - // Build certificates array for size estimation (NonEmptyArray or undefined) - const certificates = - state.certificates.length > 0 - ? (state.certificates as [Certificate.Certificate, ...Array]) - : undefined - - // Build withdrawals for size estimation - const withdrawals = - state.withdrawals.size > 0 ? new Withdrawals.Withdrawals({ withdrawals: state.withdrawals }) : undefined - - // Build requiredSigners for size estimation (NonEmptyArray or undefined) - const requiredSigners = - state.requiredSigners.length > 0 - ? (state.requiredSigners as [KeyHash.KeyHash, ...Array]) - : undefined - - // Build referenceInputs for size estimation - // Reference inputs add to transaction size and must be included in fee calculation - let referenceInputsForFee: - | readonly [TransactionInput.TransactionInput, ...Array] - | undefined - if (state.referenceInputs.length > 0) { - const refInputs = yield* buildTransactionInputs(state.referenceInputs) - referenceInputsForFee = refInputs as readonly [ - TransactionInput.TransactionInput, - ...Array - ] - } - - // Convert validity interval to slots for fee calculation - // Validity fields affect transaction size and must be included - const buildOptions = yield* BuildOptionsTag - const slotConfig = buildOptions.slotConfig! - let ttl: bigint | undefined - let validityIntervalStart: bigint | undefined - if (state.validity?.to !== undefined) { - ttl = Time.unixTimeToSlot(state.validity.to, slotConfig) - } - if (state.validity?.from !== undefined) { - validityIntervalStart = Time.unixTimeToSlot(state.validity.from, slotConfig) - } - - while (iterations < maxIterations) { - // Build transaction with current fee estimate - const body = new TransactionBody.TransactionBody({ - inputs: inputs as Array, - outputs: transactionOutputs, - fee: currentFee, - ttl, // Include TTL for accurate size calculation - validityIntervalStart, // Include validity start for accurate size calculation - mint, // Include mint field for accurate size calculation - scriptDataHash: placeholderScriptDataHash, // Include scriptDataHash for accurate size - auxiliaryDataHash: placeholderAuxiliaryDataHash, // Include auxiliaryDataHash for accurate size - collateralInputs, // Include collateral for accurate size - collateralReturn, // Include collateral return for accurate size - totalCollateral, // Include total collateral for accurate size - certificates, // Include certificates for accurate size calculation - withdrawals, // Include withdrawals for accurate size calculation - requiredSigners, // Include requiredSigners for accurate size calculation - referenceInputs: referenceInputsForFee, // Include reference inputs for accurate size calculation - votingProcedures: state.votingProcedures, // Include voting procedures for accurate size calculation - proposalProcedures: state.proposalProcedures // Include proposal procedures for accurate size calculation - }) - - const transaction = new Transaction.Transaction({ - body, - witnessSet: fakeWitnessSet, // Use fake witness set for accurate size - isValid: true, - auxiliaryData: state.auxiliaryData ?? null - }) - - // Calculate size - const size = yield* calculateTransactionSize(transaction) - - // Calculate base fee from serialized transaction size - // Note: reference script fees are a separate additive component, NOT included in base fee - const baseFee = calculateMinimumFee(size, { - minFeeCoefficient: protocolParams.minFeeCoefficient, - minFeeConstant: protocolParams.minFeeConstant - }) - - // Calculate ExUnits cost from redeemers (if pricing available) - let exUnitsCost = 0n - if (protocolParams.priceMem && protocolParams.priceStep) { - for (const [_, redeemerData] of redeemers) { - if (redeemerData.exUnits) { - const memCost = BigInt(Math.ceil(protocolParams.priceMem * Number(redeemerData.exUnits.mem))) - const stepsCost = BigInt(Math.ceil(protocolParams.priceStep * Number(redeemerData.exUnits.steps))) - exUnitsCost += memCost + stepsCost - } - } - } - - const calculatedFee = baseFee + exUnitsCost - - // Check if fully converged: fee is stable AND size is stable - if (currentFee === previousFee && size === previousSize && currentFee >= calculatedFee) { - if (iterations > 1) { - yield* Effect.logDebug( - `Fee converged after ${iterations} iterations: ${currentFee} lovelace (tx size: ${size} bytes)` - ) - } - return currentFee - } - - // Update for next iteration - previousFee = currentFee - currentFee = calculatedFee - previousSize = size - iterations++ - } - - // Didn't converge within max iterations - return the calculated fee - yield* Effect.logDebug(`Fee calculation reached max iterations (${maxIterations}): ${currentFee} lovelace`) - return currentFee - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: `Fee calculation failed to converge: ${error.message}`, - cause: error - }) - ) - ) - -// ============================================================================ -// Balance Verification for Re-selection Loop -// ============================================================================ - -/** - * Verify if selected UTxOs can cover outputs + fee for ALL assets. - * Used by the re-selection loop to determine if more UTxOs are needed. - * - * Checks both lovelace AND native assets (tokens/NFTs) to ensure complete balance. - * - * @since 2.0.0 - * @category fee-calculation - */ -export const verifyTransactionBalance = ( - selectedUtxos: ReadonlyArray, - outputs: ReadonlyArray, - fee: bigint -): { sufficient: boolean; shortfall: bigint; change: bigint } => { - // Sum all input assets using Core Assets - const totalInputAssets = selectedUtxos.reduce((acc, utxo) => CoreAssets.merge(acc, utxo.assets), CoreAssets.zero) - - // Sum all output assets using Core Assets - const totalOutputAssets = outputs.reduce((acc, output) => CoreAssets.merge(acc, output.assets), CoreAssets.zero) - - // Add fee to required lovelace - const requiredAssets = CoreAssets.withLovelace(totalOutputAssets, totalOutputAssets.lovelace + fee) - - // Calculate balance for ALL assets: inputs - (outputs + fee) - const balance = CoreAssets.subtract(totalInputAssets, requiredAssets) - - // Check if ANY asset is negative (insufficient) - let hasShortfall = false - let lovelaceShortfall = 0n - - // Check lovelace - const balanceLovelace = balance.lovelace - if (balanceLovelace < 0n) { - hasShortfall = true - lovelaceShortfall = -balanceLovelace - } - - // Check all native assets using Core Assets helpers - for (const unit of CoreAssets.getUnits(balance)) { - if (unit !== "lovelace") { - const amount = CoreAssets.getByUnit(balance, unit) - if (amount < 0n) { - hasShortfall = true - // For native asset shortfalls, we still return lovelace shortfall - // since coin selection will need to find UTxOs with both lovelace AND the missing asset - // Add some lovelace buffer to encourage selection of UTxOs with native assets - lovelaceShortfall = lovelaceShortfall > 0n ? lovelaceShortfall : 100_000n - break - } - } - } - - return { - sufficient: !hasShortfall, - shortfall: lovelaceShortfall, - change: balanceLovelace > 0n ? balanceLovelace : 0n - } -} - -// ============================================================================ -// Balance Validation -// ============================================================================ - -/** - * Validate that inputs cover outputs plus fee. - * This is the ONLY validation for minimal build - no coin selection. - * - * @since 2.0.0 - * @category validation - */ -export const validateTransactionBalance = (params: { - totalInputAssets: CoreAssets.Assets - totalOutputAssets: CoreAssets.Assets - fee: bigint -}): Effect.Effect => - Effect.gen(function* () { - const { fee, totalInputAssets, totalOutputAssets } = params - - // Calculate total outputs including fee (outputs + fee) - const totalRequired = CoreAssets.withLovelace(totalOutputAssets, totalOutputAssets.lovelace + fee) - - // Check each asset using Core Assets helpers - for (const unit of CoreAssets.getUnits(totalRequired)) { - const requiredAmount = CoreAssets.getByUnit(totalRequired, unit) - const availableAmount = CoreAssets.getByUnit(totalInputAssets, unit) - - if (availableAmount < requiredAmount) { - const shortfall = requiredAmount - availableAmount - - return yield* Effect.fail( - new TransactionBuilderError({ - message: `Insufficient ${unit}: need ${requiredAmount}, have ${availableAmount} (short by ${shortfall})`, - cause: { - unit, - required: String(requiredAmount), - available: String(availableAmount), - shortfall: String(shortfall) - } - }) - ) - } - } - - // All assets covered - }) - -/** - * Calculate leftover assets (will become excess fee in minimal build). - * - * @since 2.0.0 - * @category validation - */ -export const calculateLeftoverAssets = (params: { - totalInputAssets: CoreAssets.Assets - totalOutputAssets: CoreAssets.Assets - fee: bigint -}): CoreAssets.Assets => { - const { fee, totalInputAssets, totalOutputAssets } = params - - // Start with inputs, subtract outputs using Core Assets - const afterOutputs = CoreAssets.subtract(totalInputAssets, totalOutputAssets) - // Subtract fee from lovelace - const leftover = CoreAssets.withLovelace(afterOutputs, afterOutputs.lovelace - fee) - - // Filter out zero or negative amounts using Core Assets filter - return CoreAssets.filter(leftover, (_unit, amount) => amount > 0n) -} - -/** - * Constant overhead in bytes for a UTxO entry in the ledger state. - * Accounts for the transaction hash (32 bytes) and output index that are - * part of the UTxO key but not serialized in the transaction output itself. - * - * @see Babbage ledger spec: utxoEntrySizeWithoutVal = 160 - * @since 2.0.0 - * @category constants - */ -const UTXO_ENTRY_OVERHEAD_BYTES = 160n - -/** - * Maximum iterations for exact min-UTxO fixed-point solving. - * In practice this converges in 1-3 iterations because only lovelace CBOR - * width changes can affect output size. - * - * @since 2.0.0 - * @category constants - */ -const MAX_MIN_UTXO_ITERATIONS = 10 - -/** - * Calculate minimum ADA required for a UTxO based on its actual CBOR size. - * Uses the Babbage/Conway-era formula: coinsPerUtxoByte * (160 + serializedOutputSize). - * - * The 160-byte constant accounts for the UTxO entry overhead in the ledger state - * (transaction hash + index). A lovelace placeholder is used during CBOR encoding - * to ensure the coin field width matches the final result. - * - * This function creates a temporary TransactionOutput, encodes it to CBOR, - * and calculates the exact size to determine the minimum lovelace required. - * - * @since 2.0.0 - * @category change - */ -export const calculateMinimumUtxoLovelace = (params: { - address: CoreAddress.Address - assets: CoreAssets.Assets - datum?: DatumOption.DatumOption - scriptRef?: CoreScript.Script - coinsPerUtxoByte: bigint -}): Effect.Effect => - Effect.gen(function* () { - const calculateRequiredLovelace = (lovelace: bigint): Effect.Effect => - Effect.gen(function* () { - const assetsForSizing = CoreAssets.withLovelace(params.assets, lovelace) - - const tempOutput = yield* txOutputToTransactionOutput({ - address: params.address, - assets: assetsForSizing, - datum: params.datum, - scriptRef: params.scriptRef - }) - - const cborBytes = yield* Effect.try({ - try: () => TxOut.toCBORBytes(tempOutput), - catch: (error) => - new TransactionBuilderError({ - message: "Failed to encode output to CBOR for min UTxO calculation", - cause: error - }) - }) - - return params.coinsPerUtxoByte * (UTXO_ENTRY_OVERHEAD_BYTES + BigInt(cborBytes.length)) - }) - - // Exact fixed-point solve for minUTxO: - // required = f(lovelace), where f uses serialized size that depends on lovelace. - // We iterate until required value stabilizes. - let currentLovelace = 0n - - for (let i = 0; i < MAX_MIN_UTXO_ITERATIONS; i++) { - const requiredLovelace = yield* calculateRequiredLovelace(currentLovelace) - if (requiredLovelace === currentLovelace) { - return requiredLovelace - } - currentLovelace = requiredLovelace - } - - return yield* Effect.fail( - new TransactionBuilderError({ - message: `Minimum UTxO calculation did not converge within ${MAX_MIN_UTXO_ITERATIONS} iterations` - }) - ) - }) - -/** - * Create change output(s) for leftover assets. - * - * When unfracking is disabled (default): - * 1. Check if leftover assets exist - * 2. Calculate minimum ADA required for change output - * 3. If leftover lovelace < minimum, cannot create change (warning) - * 4. Create single output with all leftover assets to change address - * - * When unfracking is enabled: - * 1. Apply Unfrack.It optimization strategies - * 2. Bundle tokens into optimally-sized UTxOs - * 3. Isolate fungible tokens if configured - * 4. Group NFTs by policy if configured - * 5. Roll up or subdivide ADA-only UTxOs - * 6. Return multiple change outputs for optimal wallet structure - * - * @since 2.0.0 - * @category change - */ -export const createChangeOutput = (params: { - leftoverAssets: CoreAssets.Assets - changeAddress: CoreAddress.Address - coinsPerUtxoByte: bigint - unfrackOptions?: UnfrackOptions -}): Effect.Effect, TransactionBuilderError> => - Effect.gen(function* () { - const { changeAddress, coinsPerUtxoByte, leftoverAssets, unfrackOptions } = params - - // If no leftover, no change needed - if (CoreAssets.isEmpty(leftoverAssets)) { - yield* Effect.logDebug(`[createChangeOutput] No leftover assets, skipping change`) - return [] - } - - // If unfracking is enabled, use Unfrack module - if (unfrackOptions) { - const unfrackedOutputs = yield* Unfrack.createUnfrackedChangeOutputs( - changeAddress, - leftoverAssets, - unfrackOptions, - coinsPerUtxoByte - ).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: `Failed to create unfracked change outputs: ${error.message}`, - cause: error - }) - ) - ) - - yield* Effect.logDebug(`[createChangeOutput] Created ${unfrackedOutputs.length} unfracked change outputs`) - return unfrackedOutputs - } - - // Default behavior: single change output using accurate CBOR-based calculation - // Calculate minimum UTxO using actual CBOR encoding size - const minLovelace = yield* calculateMinimumUtxoLovelace({ - address: changeAddress, - assets: leftoverAssets, - coinsPerUtxoByte - }) - - // Check if we have enough lovelace for change - const leftoverLovelace = leftoverAssets.lovelace - - yield* Effect.logDebug( - `[createChangeOutput] Leftover: ${leftoverLovelace} lovelace, MinUTxO: ${minLovelace} lovelace` - ) - - if (leftoverLovelace < minLovelace) { - // Not enough lovelace to create valid change output - // This is not an error - just means leftover becomes extra fee - yield* Effect.logDebug( - `[createChangeOutput] Insufficient lovelace for change (${leftoverLovelace} < ${minLovelace}), returning empty` - ) - return [] - } - - // Create change output using Core TransactionOutput - const changeOutput = yield* makeTxOutput({ - address: changeAddress, - assets: leftoverAssets - }) - - yield* Effect.logDebug(`[createChangeOutput] Created 1 change output with ${leftoverLovelace} lovelace`) - - return [changeOutput] - }) diff --git a/packages/evolution/src/sdk/builders/Unfrack.ts b/packages/evolution/src/sdk/builders/Unfrack.ts index ec743f34..694f0c9f 100644 --- a/packages/evolution/src/sdk/builders/Unfrack.ts +++ b/packages/evolution/src/sdk/builders/Unfrack.ts @@ -15,8 +15,8 @@ import * as Effect from "effect/Effect" import type * as CoreAddress from "../../Address.js" import * as CoreAssets from "../../Assets/index.js" import type * as TxOut from "../../TxOut.js" +import { calculateMinimumUtxoLovelace, makeTxOutput as txOutputToTransactionOutput } from "./internal/TxOutput.js" import type { UnfrackOptions } from "./TransactionBuilder.js" -import { calculateMinimumUtxoLovelace, txOutputToTransactionOutput } from "./TxBuilderImpl.js" // ============================================================================ // Default Unfrack Options @@ -412,7 +412,7 @@ export const createUnfrackedChangeOutputs = ( remaining = remaining - amount } - const output = yield* txOutputToTransactionOutput({ + const output = txOutputToTransactionOutput({ address: changeAddress, assets: CoreAssets.fromLovelace(amount) }) @@ -425,7 +425,7 @@ export const createUnfrackedChangeOutputs = ( yield* Effect.logDebug( `[Unfrack] Subdivision NOT affordable (smallest output ${smallestAmount} < minUTxO ${adaMinUTxO}), returning single ADA output` ) - const output = yield* txOutputToTransactionOutput({ + const output = txOutputToTransactionOutput({ address: changeAddress, assets: CoreAssets.fromLovelace(availableLovelace) }) @@ -433,7 +433,7 @@ export const createUnfrackedChangeOutputs = ( } } else { yield* Effect.logDebug(`[Unfrack] No tokens, ADA below threshold, returning single ADA output`) - const output = yield* txOutputToTransactionOutput({ + const output = txOutputToTransactionOutput({ address: changeAddress, assets: CoreAssets.fromLovelace(availableLovelace) }) @@ -524,7 +524,7 @@ export const createUnfrackedChangeOutputs = ( // Return single output with all assets // Note: ChangeCreation's Step 4 has already verified this is affordable - const output = yield* txOutputToTransactionOutput({ + const output = txOutputToTransactionOutput({ address: changeAddress, assets: changeAssets }) @@ -551,7 +551,7 @@ export const createUnfrackedChangeOutputs = ( // Create bundle outputs with minUTxO const bundleOutputs: Array = [] for (const b of bundles) { - const output = yield* txOutputToTransactionOutput({ + const output = txOutputToTransactionOutput({ address: changeAddress, assets: b.assets }) @@ -584,7 +584,7 @@ export const createUnfrackedChangeOutputs = ( remainingAda = remainingAda - amount } - const output = yield* txOutputToTransactionOutput({ + const output = txOutputToTransactionOutput({ address: changeAddress, assets: CoreAssets.fromLovelace(amount) }) @@ -598,7 +598,7 @@ export const createUnfrackedChangeOutputs = ( `[Unfrack] Subdivision NOT affordable (smallest output ${smallestAmount} < minUTxO ${adaMinUTxO}), creating ${bundles.length} bundles + 1 ADA output` ) - const adaOutput = yield* txOutputToTransactionOutput({ + const adaOutput = txOutputToTransactionOutput({ address: changeAddress, assets: CoreAssets.fromLovelace(remaining) }) @@ -630,7 +630,7 @@ export const createUnfrackedChangeOutputs = ( for (let i = 0; i < bundles.length; i++) { const bundle = bundles[i] const extra = i === bundles.length - 1 ? perBundle + extraForLast : perBundle - const output = yield* txOutputToTransactionOutput({ + const output = txOutputToTransactionOutput({ address: changeAddress, assets: CoreAssets.merge(bundle.assets, CoreAssets.fromLovelace(extra)) }) diff --git a/packages/evolution/src/sdk/builders/index.ts b/packages/evolution/src/sdk/builders/index.ts index 1c85bdc7..fcc787e1 100644 --- a/packages/evolution/src/sdk/builders/index.ts +++ b/packages/evolution/src/sdk/builders/index.ts @@ -2,8 +2,6 @@ export * from "./CoinSelection.js" export * from "./operations/index.js" export * from "./RedeemerBuilder.js" export * from "./SignBuilder.js" -export * from "./SignBuilderImpl.js" export * from "./SubmitBuilder.js" -export * from "./SubmitBuilderImpl.js" export * from "./TransactionBuilder.js" export * from "./TransactionResult.js" diff --git a/packages/evolution/src/sdk/builders/EvaluationStateManager.ts b/packages/evolution/src/sdk/builders/internal/EvaluationStateManager.ts similarity index 93% rename from packages/evolution/src/sdk/builders/EvaluationStateManager.ts rename to packages/evolution/src/sdk/builders/internal/EvaluationStateManager.ts index e0e9712f..631a14fe 100644 --- a/packages/evolution/src/sdk/builders/EvaluationStateManager.ts +++ b/packages/evolution/src/sdk/builders/internal/EvaluationStateManager.ts @@ -8,7 +8,7 @@ * @since 2.0.0 */ -import type { TxBuilderState } from "./TransactionBuilder.js" +import type { TxBuilderState } from "../TransactionBuilder.js" /** * Invalidate all redeemer exUnits. @@ -67,7 +67,7 @@ export const hasUnevaluatedRedeemers = (redeemers: TxBuilderState["redeemers"]): * @since 2.0.0 * @category state-management */ -export const allRedeemersEvaluated = (redeemers: TxBuilderState["redeemers"]): boolean => { +export const areAllRedeemersEvaluated = (redeemers: TxBuilderState["redeemers"]): boolean => { if (redeemers.size === 0) return false return Array.from(redeemers.values()).every( diff --git a/packages/evolution/src/sdk/builders/internal/FeeEstimation.ts b/packages/evolution/src/sdk/builders/internal/FeeEstimation.ts new file mode 100644 index 00000000..5fe996cc --- /dev/null +++ b/packages/evolution/src/sdk/builders/internal/FeeEstimation.ts @@ -0,0 +1,421 @@ +/** + * Transaction fee estimation utilities. + * + * Implements the full fee calculation pipeline: + * 1. Build a fake witness set (placeholder signatures + attached scripts) + * sized to match the real witness set for accurate CBOR size estimation. + * 2. Iteratively compute the minimum fee until the value stabilises. + * 3. Add tiered reference-script fees on top of the base size-based fee. + * + * All functions are side-effect-free with respect to external I/O; they + * operate only on builder state read from the Effect context. + * + * @module FeeEstimation + * @since 2.0.0 + */ + +import { Effect, Ref } from "effect" +import type * as Array from "effect/Array" + +import type * as CoreAddress from "../../../Address.js" +import * as Bytes from "../../../Bytes.js" +import type * as Certificate from "../../../Certificate.js" +import type * as PlutusData from "../../../Data.js" +import * as Ed25519Signature from "../../../Ed25519Signature.js" +import type * as KeyHash from "../../../KeyHash.js" +import * as NativeScripts from "../../../NativeScripts.js" +import type * as PlutusV1 from "../../../PlutusV1.js" +import type * as PlutusV2 from "../../../PlutusV2.js" +import type * as PlutusV3 from "../../../PlutusV3.js" +import * as Redeemer from "../../../Redeemer.js" +import * as Redeemers from "../../../Redeemers.js" +import * as ScriptDataHash from "../../../ScriptDataHash.js" +import * as Time from "../../../Time/index.js" +import * as Transaction from "../../../Transaction.js" +import * as TransactionBody from "../../../TransactionBody.js" +import type * as TransactionInput from "../../../TransactionInput.js" +import * as TransactionWitnessSet from "../../../TransactionWitnessSet.js" +import type * as TxOut from "../../../TxOut.js" +import { calculateMinimumFee, calculateTransactionSize } from "../../../utils/FeeValidation.js" +import { hashAuxiliaryData } from "../../../utils/Hash.js" +import type * as CoreUTxO from "../../../UTxO.js" +import * as VKey from "../../../VKey.js" +import * as Withdrawals from "../../../Withdrawals.js" +import { BuildOptionsTag, TransactionBuilderError, TxContext } from "../TransactionBuilder.js" +import { buildTransactionInputs } from "./TxAssembly.js" + +// ============================================================================ +// Internal helpers +// ============================================================================ + +/** Extract the key hash from a Core Address payment credential, or null for script addresses. */ +const extractPaymentKeyHashFromCore = (address: CoreAddress.Address): Uint8Array | null => { + if (address.paymentCredential._tag === "KeyHash" && address.paymentCredential.hash) { + return address.paymentCredential.hash + } + return null +} + +/** Build a fake VKeyWitness for fee-size estimation (all zeros, correct byte lengths). */ +const buildFakeVKeyWitness = ( + keyHash: Uint8Array +): Effect.Effect => + Effect.gen(function* () { + const vkeyBytes = new Uint8Array(32) + vkeyBytes.set(keyHash.slice(0, Math.min(keyHash.length, 32))) + + const vkey = yield* Effect.try({ + try: () => new VKey.VKey({ bytes: vkeyBytes }), + catch: (error) => new TransactionBuilderError({ message: "Failed to create fake VKey", cause: error }) + }) + + const signature = yield* Effect.try({ + try: () => new Ed25519Signature.Ed25519Signature({ bytes: new Uint8Array(64) }), + catch: (error) => new TransactionBuilderError({ message: "Failed to create fake signature", cause: error }) + }) + + return new TransactionWitnessSet.VKeyWitness({ vkey, signature }) + }) + +// ============================================================================ +// Fake witness set +// ============================================================================ + +/** + * Build a fake `TransactionWitnessSet` sized to match the real one. + * + * Extracts unique payment key hashes from the provided input UTxOs, adds + * dummy witnesses for native-script required signers and for certificates / + * withdrawals / explicit required-signer entries, and populates script lists + * from builder state. Used exclusively for fee calculation — never signed. + * + * @since 2.0.0 + * @category fee-estimation + */ +export const buildFakeWitnessSet = ( + inputUtxos: ReadonlyArray +): Effect.Effect => + Effect.gen(function* () { + const stateRef = yield* TxContext + const state = yield* Ref.get(stateRef) + + const keyHashesSet = new Set() + const keyHashes: Array = [] + + for (const utxo of inputUtxos) { + const keyHash = extractPaymentKeyHashFromCore(utxo.address) + if (keyHash) { + const hex = Bytes.toHex(keyHash) + if (!keyHashesSet.has(hex)) { + keyHashesSet.add(hex) + keyHashes.push(keyHash) + } + } + } + + const nativeScripts: Array = [] + const plutusV1Scripts: Array = [] + const plutusV2Scripts: Array = [] + const plutusV3Scripts: Array = [] + + /** Add dummy witnesses for every required signer in a native script. */ + const addNativeScriptWitnesses = (script: NativeScripts.NativeScript) => { + const requiredSigners = NativeScripts.countRequiredSigners(script.script) + for (let i = 0; i < requiredSigners; i++) { + const dummyKeyHash = new Uint8Array(28) + dummyKeyHash[0] = 0xff + dummyKeyHash[1] = (keyHashesSet.size + i) & 0xff + const dummyHex = Bytes.toHex(dummyKeyHash) + if (!keyHashesSet.has(dummyHex)) { + keyHashesSet.add(dummyHex) + keyHashes.push(dummyKeyHash) + } + } + return requiredSigners + } + + for (const script of state.scripts.values()) { + switch (script._tag) { + case "NativeScript": { + nativeScripts.push(script) + const n = addNativeScriptWitnesses(script) + yield* Effect.logDebug(`[buildFakeWitnessSet] Native script requires ${n} signers`) + break + } + case "PlutusV1": plutusV1Scripts.push(script); break + case "PlutusV2": plutusV2Scripts.push(script); break + case "PlutusV3": plutusV3Scripts.push(script); break + } + } + + for (const refUtxo of state.referenceInputs) { + if (refUtxo.scriptRef?._tag === "NativeScript") { + const n = addNativeScriptWitnesses(refUtxo.scriptRef) + yield* Effect.logDebug(`[buildFakeWitnessSet] Reference native script requires ${n} signers`) + } + } + + const vkeyWitnesses: Array = [] + for (const keyHash of keyHashes) { + vkeyWitnesses.push(yield* buildFakeVKeyWitness(keyHash)) + } + + // Certificates that require stake-key signatures + for (const cert of state.certificates) { + let credentialHash: Uint8Array | undefined + if ("stakeCredential" in cert && cert.stakeCredential._tag === "KeyHash") { + credentialHash = cert.stakeCredential.hash + } + if (credentialHash) { + const hex = Bytes.toHex(credentialHash) + if (!keyHashesSet.has(hex)) { + keyHashesSet.add(hex) + vkeyWitnesses.push(yield* buildFakeVKeyWitness(credentialHash)) + } + } + } + + // Withdrawals that require stake-key signatures + for (const [rewardAccount] of state.withdrawals) { + const credential = rewardAccount.stakeCredential + if (credential._tag === "KeyHash") { + const hex = Bytes.toHex(credential.hash) + if (!keyHashesSet.has(hex)) { + keyHashesSet.add(hex) + vkeyWitnesses.push(yield* buildFakeVKeyWitness(credential.hash)) + } + } + } + + // Explicit required signers (addSigner operation) + for (const keyHash of state.requiredSigners) { + const hex = Bytes.toHex(keyHash.hash) + if (!keyHashesSet.has(hex)) { + keyHashesSet.add(hex) + vkeyWitnesses.push(yield* buildFakeVKeyWitness(keyHash.hash)) + } + } + + // Fake redeemers for accurate size estimation + const fakeRedeemers: Array = [] + let fakeIndex = 0n + for (const [_key, redeemerData] of state.redeemers) { + const exUnits = redeemerData.exUnits ?? { mem: 0n, steps: 0n } + fakeRedeemers.push( + new Redeemer.Redeemer({ + tag: redeemerData.tag, + index: fakeIndex++, + data: redeemerData.data, + exUnits: new Redeemer.ExUnits({ mem: exUnits.mem, steps: exUnits.steps }) + }) + ) + } + + return new TransactionWitnessSet.TransactionWitnessSet({ + vkeyWitnesses, + nativeScripts, + bootstrapWitnesses: [], + plutusV1Scripts, + plutusData: [], + redeemers: fakeRedeemers.length > 0 ? Redeemers.makeRedeemerMap(fakeRedeemers) : undefined, + plutusV2Scripts, + plutusV3Scripts + }) + }) + +// ============================================================================ +// Iterative fee calculation +// ============================================================================ + +/** + * Calculate the minimum transaction fee by iterating until the value + * stabilises. + * + * Algorithm: + * 1. Build a fake witness set from input UTxOs for accurate CBOR-size estimation. + * 2. Build a transaction body with `fee = 0`. + * 3. Serialise, measure size, compute fee. + * 4. Rebuild with the computed fee. + * 5. Repeat until both fee and size are stable (typically 1–2 iterations). + * + * @since 2.0.0 + * @category fee-estimation + */ +export const calculateFeeIteratively = ( + inputUtxos: ReadonlyArray, + inputs: ReadonlyArray, + outputs: ReadonlyArray, + redeemers: Map< + string, + { + readonly tag: "spend" | "mint" | "cert" | "reward" | "vote" + readonly data: PlutusData.Data + readonly exUnits?: { readonly mem: bigint; readonly steps: bigint } + } + >, + protocolParams: { + minFeeCoefficient: bigint + minFeeConstant: bigint + priceMem?: number + priceStep?: number + } +): Effect.Effect => + Effect.gen(function* () { + const stateRef = yield* TxContext + const state = yield* Ref.get(stateRef) + + // Include collateral UTxOs — they also need VKey witnesses + const allUtxosForWitnesses = state.collateral + ? [...inputUtxos, ...state.collateral.inputs] + : inputUtxos + + const fakeWitnessSet = yield* buildFakeWitnessSet(allUtxosForWitnesses) + + const transactionOutputs = outputs as Array + const mint = state.mint && state.mint.map.size > 0 ? state.mint : undefined + + let collateralInputs: Array.NonEmptyReadonlyArray | undefined + let collateralReturn: TxOut.TransactionOutput | undefined + let totalCollateral: bigint | undefined + + if (state.collateral) { + const builtCollateral = buildTransactionInputs(state.collateral.inputs) + if (builtCollateral.length > 0) { + collateralInputs = builtCollateral as Array.NonEmptyReadonlyArray + } + collateralReturn = state.collateral.returnOutput + totalCollateral = state.collateral.totalAmount + } + + // Placeholder scriptDataHash keeps size accurate when Plutus scripts are present + const hasPlutusScripts = + (fakeWitnessSet.plutusV1Scripts && fakeWitnessSet.plutusV1Scripts.length > 0) || + (fakeWitnessSet.plutusV2Scripts && fakeWitnessSet.plutusV2Scripts.length > 0) || + (fakeWitnessSet.plutusV3Scripts && fakeWitnessSet.plutusV3Scripts.length > 0) || + state.redeemers.size > 0 + + const placeholderScriptDataHash = hasPlutusScripts + ? new ScriptDataHash.ScriptDataHash({ hash: new Uint8Array(32) }) + : undefined + + const placeholderAuxiliaryDataHash = state.auxiliaryData ? hashAuxiliaryData(state.auxiliaryData) : undefined + + const certificates = + state.certificates.length > 0 + ? (state.certificates as [Certificate.Certificate, ...Array]) + : undefined + + const withdrawals = + state.withdrawals.size > 0 + ? new Withdrawals.Withdrawals({ withdrawals: state.withdrawals }) + : undefined + + const requiredSigners = + state.requiredSigners.length > 0 + ? (state.requiredSigners as [KeyHash.KeyHash, ...Array]) + : undefined + + let referenceInputsForFee: + | readonly [TransactionInput.TransactionInput, ...Array] + | undefined + if (state.referenceInputs.length > 0) { + const refInputs = buildTransactionInputs(state.referenceInputs) + referenceInputsForFee = refInputs as readonly [ + TransactionInput.TransactionInput, + ...Array + ] + } + + const buildOptions = yield* BuildOptionsTag + const slotConfig = buildOptions.slotConfig! + + let ttl: bigint | undefined + let validityIntervalStart: bigint | undefined + + if (state.validity?.to !== undefined) { + ttl = Time.unixTimeToSlot(state.validity.to, slotConfig) + } + if (state.validity?.from !== undefined) { + validityIntervalStart = Time.unixTimeToSlot(state.validity.from, slotConfig) + } + + let currentFee = 0n + let previousSize = 0 + let previousFee = 0n + let iterations = 0 + const maxIterations = 10 + + while (iterations < maxIterations) { + const body = new TransactionBody.TransactionBody({ + inputs: inputs as Array, + outputs: transactionOutputs, + fee: currentFee, + ttl, + validityIntervalStart, + mint, + scriptDataHash: placeholderScriptDataHash, + auxiliaryDataHash: placeholderAuxiliaryDataHash, + collateralInputs, + collateralReturn, + totalCollateral, + certificates, + withdrawals, + requiredSigners, + referenceInputs: referenceInputsForFee, + votingProcedures: state.votingProcedures, + proposalProcedures: state.proposalProcedures + }) + + const transaction = new Transaction.Transaction({ + body, + witnessSet: fakeWitnessSet, + isValid: true, + auxiliaryData: state.auxiliaryData ?? null + }) + + const size = calculateTransactionSize(transaction) + + const baseFee = calculateMinimumFee(size, { + minFeeCoefficient: protocolParams.minFeeCoefficient, + minFeeConstant: protocolParams.minFeeConstant + }) + + let exUnitsCost = 0n + if (protocolParams.priceMem && protocolParams.priceStep) { + for (const [, redeemerData] of redeemers) { + if (redeemerData.exUnits) { + exUnitsCost += + BigInt(Math.ceil(protocolParams.priceMem * Number(redeemerData.exUnits.mem))) + + BigInt(Math.ceil(protocolParams.priceStep * Number(redeemerData.exUnits.steps))) + } + } + } + + const calculatedFee = baseFee + exUnitsCost + + if (currentFee === previousFee && size === previousSize && currentFee >= calculatedFee) { + if (iterations > 1) { + yield* Effect.logDebug( + `Fee converged after ${iterations} iterations: ${currentFee} lovelace (tx size: ${size} bytes)` + ) + } + return currentFee + } + + previousFee = currentFee + currentFee = calculatedFee + previousSize = size + iterations++ + } + + yield* Effect.logDebug(`Fee calculation reached max iterations (${maxIterations}): ${currentFee} lovelace`) + return currentFee + }).pipe( + Effect.mapError( + (error) => + new TransactionBuilderError({ + message: `Fee calculation failed to converge: ${error.message}`, + cause: error + }) + ) + ) diff --git a/packages/evolution/src/sdk/builders/SignBuilderImpl.ts b/packages/evolution/src/sdk/builders/internal/SignBuilderImpl.ts similarity index 93% rename from packages/evolution/src/sdk/builders/SignBuilderImpl.ts rename to packages/evolution/src/sdk/builders/internal/SignBuilderImpl.ts index 79ad0587..ef4363d3 100644 --- a/packages/evolution/src/sdk/builders/SignBuilderImpl.ts +++ b/packages/evolution/src/sdk/builders/internal/SignBuilderImpl.ts @@ -16,27 +16,25 @@ import { Effect } from "effect" -import * as Script from "../../Script.js" -import * as Transaction from "../../Transaction.js" -import * as TransactionHash from "../../TransactionHash.js" -import * as TransactionWitnessSet from "../../TransactionWitnessSet.js" -import type * as TxOut from "../../TxOut.js" -import { hashTransaction } from "../../utils/Hash.js" -import * as CoreUTxO from "../../UTxO.js" -import type * as Provider from "../provider/Provider.js" -import type * as WalletNew from "../wallet/WalletNew.js" -import type { SignBuilder, SignBuilderEffect } from "./SignBuilder.js" +import * as Script from "../../../Script.js" +import * as Transaction from "../../../Transaction.js" +import * as TransactionHash from "../../../TransactionHash.js" +import * as TransactionWitnessSet from "../../../TransactionWitnessSet.js" +import type * as TxOut from "../../../TxOut.js" +import { hashTransaction } from "../../../utils/Hash.js" +import * as CoreUTxO from "../../../UTxO.js" +import type { SignBuilder, SignBuilderSubmittableEffect } from "../SignBuilder.js" +import { type ChainResult, type ProviderLike, type SigningWalletLike, TransactionBuilderError } from "../TransactionBuilder.js" import { makeSubmitBuilder } from "./SubmitBuilderImpl.js" -import { type ChainResult, TransactionBuilderError } from "./TransactionBuilder.js" // ============================================================================ // SignBuilder Factory // ============================================================================ /** - * Wallet type - can be SigningWallet or ApiWallet (both have Effect.signTx) + * Wallet type - accepts any wallet-like object with signTx capability */ -type Wallet = WalletNew.SigningWallet | WalletNew.ApiWallet +type Wallet = SigningWalletLike /** * Create a SignBuilder instance for a built transaction. @@ -50,7 +48,7 @@ export const makeSignBuilder = (params: { fee: bigint utxos: ReadonlyArray referenceUtxos: ReadonlyArray - provider: Provider.Provider + provider: ProviderLike wallet: Wallet // Data for lazy chainResult computation outputs: ReadonlyArray @@ -100,7 +98,7 @@ export const makeSignBuilder = (params: { // Effect Namespace Implementation // ============================================================================ - const signEffect: SignBuilderEffect = { + const signEffect: SignBuilderSubmittableEffect = { /** * Sign the transaction by delegating to the wallet's Effect.signTx method. * diff --git a/packages/evolution/src/sdk/builders/SubmitBuilderImpl.ts b/packages/evolution/src/sdk/builders/internal/SubmitBuilderImpl.ts similarity index 80% rename from packages/evolution/src/sdk/builders/SubmitBuilderImpl.ts rename to packages/evolution/src/sdk/builders/internal/SubmitBuilderImpl.ts index ce9bf654..2da9319c 100644 --- a/packages/evolution/src/sdk/builders/SubmitBuilderImpl.ts +++ b/packages/evolution/src/sdk/builders/internal/SubmitBuilderImpl.ts @@ -13,11 +13,10 @@ import { Effect } from "effect" -import type * as Transaction from "../../Transaction.js" -import type * as TransactionWitnessSet from "../../TransactionWitnessSet.js" -import type * as Provider from "../provider/Provider.js" -import type { SubmitBuilder, SubmitBuilderEffect } from "./SubmitBuilder.js" -import { TransactionBuilderError } from "./TransactionBuilder.js" +import type * as Transaction from "../../../Transaction.js" +import type * as TransactionWitnessSet from "../../../TransactionWitnessSet.js" +import type { SubmitBuilder, SubmitBuilderEffect } from "../SubmitBuilder.js" +import { type ProviderLike, TransactionBuilderError } from "../TransactionBuilder.js" /** * Create a SubmitBuilder instance for a signed transaction. @@ -28,7 +27,7 @@ import { TransactionBuilderError } from "./TransactionBuilder.js" export const makeSubmitBuilder = ( signedTransaction: Transaction.Transaction, witnessSet: TransactionWitnessSet.TransactionWitnessSet, - provider: Provider.Provider + provider: ProviderLike ): SubmitBuilder => { const submitEffect: SubmitBuilderEffect = { submit: () => diff --git a/packages/evolution/src/sdk/builders/internal/TxAssembly.ts b/packages/evolution/src/sdk/builders/internal/TxAssembly.ts new file mode 100644 index 00000000..cfb52a85 --- /dev/null +++ b/packages/evolution/src/sdk/builders/internal/TxAssembly.ts @@ -0,0 +1,459 @@ +/** + * Low-level transaction assembly utilities. + * + * Converts builder state into the concrete CBOR-ready transaction types + * used by the Cardano ledger. This layer owns the mapping from Effect-TS + * builder state (TxContext / BuildOptionsTag) to `Transaction` and + * `TransactionInput` values — nothing above this level should touch raw + * CBOR serialisation. + * + * @module TxAssembly + * @since 2.0.0 + */ + +import { Effect, Ref } from "effect" +import type * as Array from "effect/Array" + +import * as Bytes from "../../../Bytes.js" +import type * as Certificate from "../../../Certificate.js" +import * as CostModel from "../../../CostModel.js" +import type * as PlutusData from "../../../Data.js" +import type * as KeyHash from "../../../KeyHash.js" +import type * as PlutusV1 from "../../../PlutusV1.js" +import type * as PlutusV2 from "../../../PlutusV2.js" +import type * as PlutusV3 from "../../../PlutusV3.js" +import * as PolicyId from "../../../PolicyId.js" +import * as Redeemer from "../../../Redeemer.js" +import * as Redeemers from "../../../Redeemers.js" +import type * as RewardAccount from "../../../RewardAccount.js" +import * as Time from "../../../Time/index.js" +import * as Transaction from "../../../Transaction.js" +import * as TransactionBody from "../../../TransactionBody.js" +import * as TransactionHash from "../../../TransactionHash.js" +import * as TransactionInput from "../../../TransactionInput.js" +import * as TransactionWitnessSet from "../../../TransactionWitnessSet.js" +import type * as TxOut from "../../../TxOut.js" +import { hashAuxiliaryData, hashScriptData } from "../../../utils/Hash.js" +import type * as CoreUTxO from "../../../UTxO.js" +import * as Withdrawals from "../../../Withdrawals.js" +import { voterToKey } from "../phases/Calculations.js" +import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" + +// ============================================================================ +// Input Construction +// ============================================================================ + +/** + * Convert UTxOs to sorted `TransactionInput` values. + * + * Inputs are sorted by (txHash, outputIndex) for deterministic ordering, + * matching the Cardano ledger's canonical form. + * + * @since 2.0.0 + * @category assembly + */ +export const buildTransactionInputs = ( + utxos: ReadonlyArray +): ReadonlyArray => { + const inputs: Array = [] + + for (const utxo of utxos) { + inputs.push( + new TransactionInput.TransactionInput({ + transactionId: utxo.transactionId, + index: utxo.index + }) + ) + } + + // Deterministic ordering required by the ledger + inputs.sort((a, b) => { + const hashA = a.transactionId.hash + const hashB = b.transactionId.hash + for (let i = 0; i < hashA.length; i++) { + if (hashA[i] !== hashB[i]) return hashA[i] - hashB[i] + } + return Number(a.index - b.index) + }) + + return inputs +} + +// ============================================================================ +// Transaction Assembly +// ============================================================================ + +/** + * Assemble a fully-formed `Transaction` from inputs, outputs, and a + * pre-calculated fee. + * + * Reads all required state (scripts, redeemers, certificates, withdrawals, + * validity intervals, collateral, reference inputs, voting/proposal + * procedures, auxiliary data) from the Effect context and produces a + * CBOR-ready `Transaction` with a correctly computed `scriptDataHash`. + * + * @since 2.0.0 + * @category assembly + */ +export const assembleTransaction = ( + inputs: ReadonlyArray, + outputs: ReadonlyArray, + fee: bigint +): Effect.Effect => + Effect.gen(function* () { + const stateRef = yield* TxContext + const state = yield* Ref.get(stateRef) + + yield* Effect.logDebug(`[Assembly] Building transaction with ${inputs.length} inputs, ${outputs.length} outputs`) + yield* Effect.logDebug(`[Assembly] Reference inputs in state: ${state.referenceInputs.length}`) + yield* Effect.logDebug(`[Assembly] Scripts in state: ${state.scripts.size}`) + yield* Effect.logDebug(`[Assembly] Redeemers in state: ${state.redeemers.size}`) + + const transactionOutputs = outputs as Array + + // ── Collateral ─────────────────────────────────────────────────────────── + + let collateralInputs: Array.NonEmptyReadonlyArray | undefined + let collateralReturn: TxOut.TransactionOutput | undefined + let totalCollateral: bigint | undefined + + if (state.collateral) { + yield* Effect.logDebug( + `[Assembly] Adding collateral: ${state.collateral.inputs.length} inputs, ` + + `total ${state.collateral.totalAmount} lovelace` + ) + collateralInputs = buildTransactionInputs( + state.collateral.inputs + ) as Array.NonEmptyReadonlyArray + totalCollateral = state.collateral.totalAmount + + if (state.collateral.returnOutput) { + yield* Effect.logDebug( + `[Assembly] Collateral return lovelace: ${state.collateral.returnOutput.assets.lovelace}` + ) + collateralReturn = state.collateral.returnOutput + } + } + + // ── Reference inputs ───────────────────────────────────────────────────── + + let referenceInputs: + | readonly [TransactionInput.TransactionInput, ...Array] + | undefined + if (state.referenceInputs.length > 0) { + const refInputs = buildTransactionInputs(state.referenceInputs) + referenceInputs = refInputs as readonly [ + TransactionInput.TransactionInput, + ...Array + ] + } + + // ── Scripts ─────────────────────────────────────────────────────────────── + + const plutusV1Scripts: Array = [] + const plutusV2Scripts: Array = [] + const plutusV3Scripts: Array = [] + const nativeScripts: Array = [] + + for (const [scriptHash, coreScript] of state.scripts) { + yield* Effect.logDebug(`[Assembly] Processing script: hash=${scriptHash}, type=${coreScript._tag}`) + switch (coreScript._tag) { + case "PlutusV1": plutusV1Scripts.push(coreScript); break + case "PlutusV2": plutusV2Scripts.push(coreScript); break + case "PlutusV3": plutusV3Scripts.push(coreScript); break + case "NativeScript": nativeScripts.push(coreScript); break + } + } + + // ── Redeemers ───────────────────────────────────────────────────────────── + + const redeemers: Array = [] + + // Build lookup maps for redeemer index resolution + const inputIndexMap = new Map() + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]! + const key = `${TransactionHash.toHex(input.transactionId)}#${input.index}` + yield* Effect.logDebug(`[Assembly] Input ${i}: ${key}`) + inputIndexMap.set(key, i) + } + + const mintIndexMap = new Map() + if (state.mint && state.mint.map.size > 0) { + const sortedPolicyIds = globalThis.Array.from(state.mint.map.keys()) + .map((pid) => PolicyId.toHex(pid)) + .sort() + for (let i = 0; i < sortedPolicyIds.length; i++) { + mintIndexMap.set(sortedPolicyIds[i]!, i) + yield* Effect.logDebug(`[Assembly] Mint policy ${i}: ${sortedPolicyIds[i]}`) + } + } + + yield* Effect.logDebug(`[Assembly] Input index map: ${inputIndexMap.size} entries`) + yield* Effect.logDebug(`[Assembly] Redeemer map keys: ${globalThis.Array.from(state.redeemers.keys()).join(", ")}`) + + for (const [key, redeemerData] of state.redeemers) { + yield* Effect.logDebug(`[Assembly] Processing redeemer: key=${key}, tag=${redeemerData.tag}`) + + let redeemerIndex: number | undefined + + if (redeemerData.tag === "mint") { + redeemerIndex = mintIndexMap.get(key) + if (redeemerIndex === undefined) { + yield* Effect.logWarning(`[Assembly] Could not find mint index for policy: ${key}`) + continue + } + } else if (redeemerData.tag === "cert") { + const credentialHex = key.slice(5) // Remove "cert:" prefix + for (let i = 0; i < state.certificates.length; i++) { + const cert = state.certificates[i]! + if ("stakeCredential" in cert && cert.stakeCredential) { + const certCredHex = Bytes.toHex((cert.stakeCredential as { hash: Uint8Array }).hash) + if (certCredHex === credentialHex) { redeemerIndex = i; break } + } + if ("drepCredential" in cert && cert.drepCredential) { + const certCredHex = Bytes.toHex((cert.drepCredential as { hash: Uint8Array }).hash) + if (certCredHex === credentialHex) { redeemerIndex = i; break } + } + } + if (redeemerIndex === undefined) { + yield* Effect.logWarning(`[Assembly] Could not find cert index for key: ${key}`) + continue + } + } else if (redeemerData.tag === "reward") { + const credentialHex = key.slice(7) // Remove "reward:" prefix + const sortedWithdrawals = globalThis.Array.from(state.withdrawals.entries()).sort((a, b) => { + const aHex = Bytes.toHex(a[0].stakeCredential.hash) + const bHex = Bytes.toHex(b[0].stakeCredential.hash) + return aHex.localeCompare(bHex) + }) + for (let i = 0; i < sortedWithdrawals.length; i++) { + const [rewardAccount] = sortedWithdrawals[i]! + if (Bytes.toHex(rewardAccount.stakeCredential.hash) === credentialHex) { + redeemerIndex = i + break + } + } + if (redeemerIndex === undefined) { + yield* Effect.logWarning(`[Assembly] Could not find withdrawal index for key: ${key}`) + continue + } + } else if (redeemerData.tag === "vote") { + if (!state.votingProcedures) { + yield* Effect.logWarning(`[Assembly] Vote redeemer found but no votingProcedures in state`) + continue + } + const sortedVoterKeys: Array = [] + for (const voter of state.votingProcedures.procedures.keys()) { + sortedVoterKeys.push(voterToKey(voter)) + } + sortedVoterKeys.sort() + for (let i = 0; i < sortedVoterKeys.length; i++) { + if (sortedVoterKeys[i] === key) { redeemerIndex = i; break } + } + if (redeemerIndex === undefined) { + yield* Effect.logWarning(`[Assembly] Could not find voter index for key: ${key}`) + continue + } + } else { + // spend redeemer — look up by UTxO ref key + redeemerIndex = inputIndexMap.get(key) + if (redeemerIndex === undefined) { + yield* Effect.logWarning(`[Assembly] Could not find input index for redeemer key: ${key}`) + continue + } + } + + yield* Effect.logDebug( + `[Assembly] Redeemer exUnits: mem=${redeemerData.exUnits?.mem ?? 0n}, steps=${redeemerData.exUnits?.steps ?? 0n}` + ) + + redeemers.push( + new Redeemer.Redeemer({ + tag: redeemerData.tag, + index: BigInt(redeemerIndex), + data: redeemerData.data, + exUnits: redeemerData.exUnits + ? new Redeemer.ExUnits({ mem: redeemerData.exUnits.mem, steps: redeemerData.exUnits.steps }) + : new Redeemer.ExUnits({ mem: 0n, steps: 0n }) + }) + ) + } + + // ── Plutus data ─────────────────────────────────────────────────────────── + + // Only datum-hash UTxOs need datum resolution in the witness set. + // Inline datums are already embedded in the UTxO and must NOT be + // re-included (would cause "extraneous datums" ledger error). + const plutusDataArray: Array = [] + for (const utxo of state.selectedUtxos) { + if (utxo.datumOption?._tag === "DatumHash") { + yield* Effect.logDebug(`[Assembly] Found datum hash UTxO (resolution not yet implemented)`) + } + } + + // ── scriptDataHash ──────────────────────────────────────────────────────── + + let scriptDataHash: ReturnType | undefined + let redeemersConcrete: Redeemers.RedeemerMap | undefined + + if (redeemers.length > 0) { + const config = yield* TxBuilderConfigTag + + if (!config.provider) { + throw new TransactionBuilderError({ + message: + "Script transactions require a provider to fetch full protocol parameters for scriptDataHash calculation", + cause: { redeemerCount: redeemers.length } + }) + } + + const fullProtocolParams = yield* config.provider.Effect.getProtocolParameters().pipe( + Effect.mapError( + (providerError) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters for scriptDataHash: ${providerError.message}`, + cause: providerError + }) + ) + ) + + let hasPlutusV1 = plutusV1Scripts.length > 0 + let hasPlutusV2 = plutusV2Scripts.length > 0 + let hasPlutusV3 = plutusV3Scripts.length > 0 + + for (const refUtxo of state.referenceInputs) { + if (refUtxo.scriptRef) { + switch (refUtxo.scriptRef._tag) { + case "PlutusV1": hasPlutusV1 = true; break + case "PlutusV2": hasPlutusV2 = true; break + case "PlutusV3": hasPlutusV3 = true; break + } + } + } + + for (const utxo of state.selectedUtxos) { + if (utxo.scriptRef) { + switch (utxo.scriptRef._tag) { + case "PlutusV1": hasPlutusV1 = true; break + case "PlutusV2": hasPlutusV2 = true; break + case "PlutusV3": hasPlutusV3 = true; break + } + } + } + + yield* Effect.logDebug(`[Assembly] Cost models included: V1=${hasPlutusV1}, V2=${hasPlutusV2}, V3=${hasPlutusV3}`) + + const costModels = new CostModel.CostModels({ + PlutusV1: new CostModel.CostModel({ + costs: hasPlutusV1 + ? Object.values(fullProtocolParams.costModels.PlutusV1).map((v) => BigInt(v)) + : [] + }), + PlutusV2: new CostModel.CostModel({ + costs: hasPlutusV2 + ? Object.values(fullProtocolParams.costModels.PlutusV2).map((v) => BigInt(v)) + : [] + }), + PlutusV3: new CostModel.CostModel({ + costs: hasPlutusV3 + ? Object.values(fullProtocolParams.costModels.PlutusV3).map((v) => BigInt(v)) + : [] + }) + }) + + redeemersConcrete = Redeemers.makeRedeemerMap(redeemers) + scriptDataHash = hashScriptData( + redeemersConcrete, + costModels, + plutusDataArray.length > 0 ? plutusDataArray : undefined + ) + yield* Effect.logDebug(`[Assembly] scriptDataHash: ${scriptDataHash.hash.toString()}`) + } + + yield* Effect.logDebug(`[Assembly] WitnessSet: V1=${plutusV1Scripts.length}, V2=${plutusV2Scripts.length}, V3=${plutusV3Scripts.length}, redeemers=${redeemers.length}`) + + // ── Transaction body ────────────────────────────────────────────────────── + + const certificates = + state.certificates.length > 0 + ? (state.certificates as [Certificate.Certificate, ...Array]) + : undefined + + const withdrawals = + state.withdrawals.size > 0 + ? new Withdrawals.Withdrawals({ withdrawals: state.withdrawals as Map }) + : undefined + + const buildOptions = yield* BuildOptionsTag + const slotConfig = buildOptions.slotConfig! + + let ttl: bigint | undefined + let validityIntervalStart: bigint | undefined + + if (state.validity?.to !== undefined) { + ttl = Time.unixTimeToSlot(state.validity.to, slotConfig) + yield* Effect.logDebug(`[Assembly] TTL: ${ttl}`) + } + if (state.validity?.from !== undefined) { + validityIntervalStart = Time.unixTimeToSlot(state.validity.from, slotConfig) + yield* Effect.logDebug(`[Assembly] Validity start: ${validityIntervalStart}`) + } + + const requiredSigners = + state.requiredSigners.length > 0 + ? (state.requiredSigners as [KeyHash.KeyHash, ...Array]) + : undefined + + let auxiliaryDataHash: ReturnType | undefined + if (state.auxiliaryData) { + auxiliaryDataHash = hashAuxiliaryData(state.auxiliaryData) + yield* Effect.logDebug(`[Assembly] auxiliaryDataHash: ${auxiliaryDataHash.toString()}`) + } + + const body = new TransactionBody.TransactionBody({ + inputs: inputs as Array, + outputs: transactionOutputs, + fee, + ttl, + validityIntervalStart, + collateralInputs, + collateralReturn, + totalCollateral, + referenceInputs, + mint: state.mint && state.mint.map.size > 0 ? state.mint : undefined, + scriptDataHash, + auxiliaryDataHash, + certificates, + withdrawals, + requiredSigners, + votingProcedures: state.votingProcedures, + proposalProcedures: state.proposalProcedures + }) + + const witnessSet = new TransactionWitnessSet.TransactionWitnessSet({ + vkeyWitnesses: [], + nativeScripts, + bootstrapWitnesses: [], + plutusV1Scripts, + plutusData: plutusDataArray, + redeemers: redeemers.length > 0 ? redeemersConcrete : undefined, + plutusV2Scripts, + plutusV3Scripts + }) + + return new Transaction.Transaction({ + body, + witnessSet, + isValid: true, + auxiliaryData: state.auxiliaryData ?? null + }) + }).pipe( + Effect.mapError( + (error) => + new TransactionBuilderError({ + message: `Failed to assemble transaction: ${error.message}`, + cause: error + }) + ) + ) diff --git a/packages/evolution/src/sdk/builders/internal/TxOutput.ts b/packages/evolution/src/sdk/builders/internal/TxOutput.ts new file mode 100644 index 00000000..abd47df2 --- /dev/null +++ b/packages/evolution/src/sdk/builders/internal/TxOutput.ts @@ -0,0 +1,124 @@ +/** + * Transaction output construction utilities. + * + * Provides the canonical function for building `TransactionOutput` values + * and computing the minimum lovelace required to satisfy the ledger's + * coinsPerUtxoByte rule. + * + * @module TxOutput + * @since 2.0.0 + */ + +import { Effect } from "effect" + +import type * as CoreAddress from "../../../Address.js" +import * as CoreAssets from "../../../Assets/index.js" +import type * as DatumOption from "../../../DatumOption.js" +import * as CoreScript from "../../../Script.js" +import * as ScriptRef from "../../../ScriptRef.js" +import * as TxOut from "../../../TxOut.js" +import { TransactionBuilderError } from "../TransactionBuilder.js" + +// ============================================================================ +// Output Construction +// ============================================================================ + +/** + * Build a `TransactionOutput` from typed parameters. + * + * This is the canonical output-construction helper used by all builder + * operations and phases. + * + * @since 2.0.0 + * @category constructors + */ +export const makeTxOutput = (params: { + address: CoreAddress.Address + assets: CoreAssets.Assets + datum?: DatumOption.DatumOption + scriptRef?: CoreScript.Script +}): TxOut.TransactionOutput => { + const scriptRefEncoded = params.scriptRef + ? new ScriptRef.ScriptRef({ bytes: CoreScript.toCBOR(params.scriptRef) }) + : undefined + + return new TxOut.TransactionOutput({ + address: params.address, + assets: params.assets, + datumOption: params.datum, + scriptRef: scriptRefEncoded + }) +} + +// ============================================================================ +// Minimum UTxO Lovelace +// ============================================================================ + +/** + * Constant overhead in bytes for a UTxO entry in the ledger state. + * Accounts for the transaction hash (32 bytes) and output index that are + * part of the UTxO key but not serialised in the transaction output itself. + * + * @see Babbage ledger spec: utxoEntrySizeWithoutVal = 160 + */ +const UTXO_ENTRY_OVERHEAD_BYTES = 160n + +/** + * Maximum iterations for exact min-UTxO fixed-point solving. + * Converges in 1–3 iterations because only the lovelace CBOR-width can change. + */ +const MAX_MIN_UTXO_ITERATIONS = 10 + +/** + * Calculate the minimum lovelace required for an output based on its actual + * CBOR-encoded size. + * + * Uses the Babbage/Conway formula: + * ``` + * minLovelace = coinsPerUtxoByte × (160 + serialisedOutputSize) + * ``` + * + * Iterates to a fixed point because the lovelace amount itself affects the + * CBOR encoding width, which feeds back into the size calculation. + * + * @since 2.0.0 + * @category ledger-rules + */ +export const calculateMinimumUtxoLovelace = (params: { + address: CoreAddress.Address + assets: CoreAssets.Assets + datum?: DatumOption.DatumOption + scriptRef?: CoreScript.Script + coinsPerUtxoByte: bigint +}): Effect.Effect => + Effect.gen(function* () { + const calculateRequiredLovelace = (lovelace: bigint): bigint => { + const assetsForSizing = CoreAssets.withLovelace(params.assets, lovelace) + + const tempOutput = makeTxOutput({ + address: params.address, + assets: assetsForSizing, + datum: params.datum, + scriptRef: params.scriptRef + }) + + const cborBytes = TxOut.toCBORBytes(tempOutput) + return params.coinsPerUtxoByte * (UTXO_ENTRY_OVERHEAD_BYTES + BigInt(cborBytes.length)) + } + + let currentLovelace = 0n + + for (let i = 0; i < MAX_MIN_UTXO_ITERATIONS; i++) { + const requiredLovelace = calculateRequiredLovelace(currentLovelace) + if (requiredLovelace === currentLovelace) { + return requiredLovelace + } + currentLovelace = requiredLovelace + } + + return yield* Effect.fail( + new TransactionBuilderError({ + message: `Minimum UTxO calculation did not converge within ${MAX_MIN_UTXO_ITERATIONS} iterations` + }) + ) + }) diff --git a/packages/evolution/src/sdk/builders/internal/UtxoAnalysis.ts b/packages/evolution/src/sdk/builders/internal/UtxoAnalysis.ts new file mode 100644 index 00000000..90890e85 --- /dev/null +++ b/packages/evolution/src/sdk/builders/internal/UtxoAnalysis.ts @@ -0,0 +1,62 @@ +/** + * Low-level UTxO analysis utilities. + * + * Predicates and aggregators that inspect UTxO sets without constructing + * new transaction structures. + * + * @module UtxoAnalysis + * @since 2.0.0 + */ + +import type * as CoreAddress from "../../../Address.js" +import * as CoreAssets from "../../../Assets/index.js" +import type * as CoreUTxO from "../../../UTxO.js" + +// ============================================================================ +// Address Predicates +// ============================================================================ + +/** + * Return true when the address payment credential is a ScriptHash. + * + * @since 2.0.0 + * @category predicates + */ +export const isScriptAddress = (address: CoreAddress.Address): boolean => + address.paymentCredential?._tag === "ScriptHash" + +/** + * Filter UTxOs to those locked by a script payment credential. + * + * @since 2.0.0 + * @category predicates + */ +export const filterScriptUtxos = (utxos: ReadonlyArray): ReadonlyArray => { + const scriptUtxos: Array = [] + for (const utxo of utxos) { + if (isScriptAddress(utxo.address)) { + scriptUtxos.push(utxo) + } + } + return scriptUtxos +} + +// ============================================================================ +// Asset Aggregation +// ============================================================================ + +/** + * Sum all assets across a UTxO set (or Set). + * + * @since 2.0.0 + * @category aggregation + */ +export const calculateTotalAssets = (utxos: ReadonlyArray | Set): CoreAssets.Assets => { + const utxoArray = ( + globalThis.Array.isArray(utxos) ? utxos : globalThis.Array.from(utxos) + ) as ReadonlyArray + return utxoArray.reduce( + (total: CoreAssets.Assets, utxo: CoreUTxO.UTxO) => CoreAssets.merge(total, utxo.assets), + CoreAssets.zero + ) +} diff --git a/packages/evolution/src/sdk/builders/operations/Attach.ts b/packages/evolution/src/sdk/builders/operations/Attach.ts index 27deae19..3853144e 100644 --- a/packages/evolution/src/sdk/builders/operations/Attach.ts +++ b/packages/evolution/src/sdk/builders/operations/Attach.ts @@ -14,7 +14,7 @@ import { TxContext } from "../TransactionBuilder.js" * @since 2.0.0 * @category operations */ -export const attachScriptToState = (script: ScriptCore.Script) => +export const createAttachScriptProgram = (script: ScriptCore.Script) => Effect.gen(function* () { const stateRef = yield* TxContext const state = yield* Ref.get(stateRef) diff --git a/packages/evolution/src/sdk/builders/operations/Collect.ts b/packages/evolution/src/sdk/builders/operations/Collect.ts index 862d63c6..2e3c5394 100644 --- a/packages/evolution/src/sdk/builders/operations/Collect.ts +++ b/packages/evolution/src/sdk/builders/operations/Collect.ts @@ -10,9 +10,9 @@ import { Effect, Ref } from "effect" import * as CoreAssets from "../../../Assets/index.js" import * as ScriptHash from "../../../ScriptHash.js" import * as UTxO from "../../../UTxO.js" +import { calculateTotalAssets, filterScriptUtxos } from "../internal/UtxoAnalysis.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" import { TransactionBuilderError, TxContext } from "../TransactionBuilder.js" -import { calculateTotalAssets, filterScriptUtxos } from "../TxBuilderImpl.js" import type { CollectFromParams } from "./Operations.js" /** @@ -52,7 +52,7 @@ export const createCollectFromProgram = ( } // 2. Filter script-locked UTxOs - const scriptUtxos = yield* filterScriptUtxos(params.inputs) + const scriptUtxos = filterScriptUtxos(params.inputs) // 3. Filter out native script UTxOs (those with native scripts don't need redeemers) // Native scripts are validated by signatures, not redeemers diff --git a/packages/evolution/src/sdk/builders/operations/Pay.ts b/packages/evolution/src/sdk/builders/operations/Pay.ts index 1d4c1aa8..c7d2148c 100644 --- a/packages/evolution/src/sdk/builders/operations/Pay.ts +++ b/packages/evolution/src/sdk/builders/operations/Pay.ts @@ -8,8 +8,8 @@ import { Effect, Ref } from "effect" import * as CoreAssets from "../../../Assets/index.js" +import { makeTxOutput } from "../internal/TxOutput.js" import { TxContext } from "../TransactionBuilder.js" -import { makeTxOutput } from "../TxBuilderImpl.js" import type { PayToAddressParams } from "./Operations.js" /** @@ -29,7 +29,7 @@ export const createPayToAddressProgram = (params: PayToAddressParams) => const ctx = yield* TxContext // 1. Create Core TransactionOutput from params - const output = yield* makeTxOutput({ + const output = makeTxOutput({ address: params.address, assets: params.assets, datum: params.datum, diff --git a/packages/evolution/src/sdk/builders/operations/Stake.ts b/packages/evolution/src/sdk/builders/operations/Stake.ts index 17b27610..76f70bad 100644 --- a/packages/evolution/src/sdk/builders/operations/Stake.ts +++ b/packages/evolution/src/sdk/builders/operations/Stake.ts @@ -11,7 +11,7 @@ import * as Bytes from "../../../Bytes.js" import * as Certificate from "../../../Certificate.js" import * as RewardAccount from "../../../RewardAccount.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" -import { TransactionBuilderError, type TxBuilderConfig, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { DelegateToDRepParams, DelegateToParams, @@ -674,11 +674,11 @@ export const createDeregisterStakeProgram = ( * @category programs */ export const createWithdrawProgram = ( - params: WithdrawParams, - config: TxBuilderConfig -): Effect.Effect => + params: WithdrawParams +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext + const config = yield* TxBuilderConfigTag // Check if script-controlled const isScriptControlled = params.stakeCredential._tag === "ScriptHash" diff --git a/packages/evolution/src/sdk/builders/operations/Vote.ts b/packages/evolution/src/sdk/builders/operations/Vote.ts index c470c7ae..14b5b499 100644 --- a/packages/evolution/src/sdk/builders/operations/Vote.ts +++ b/packages/evolution/src/sdk/builders/operations/Vote.ts @@ -9,7 +9,7 @@ import { Effect, Ref } from "effect" import type * as GovernanceAction from "../../../GovernanceAction.js" import * as VotingProcedures from "../../../VotingProcedures.js" -import { voterToKey } from "../phases/utils.js" +import { voterToKey } from "../phases/Calculations.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" import { TransactionBuilderError, TxContext } from "../TransactionBuilder.js" import type { VoteParams } from "./Operations.js" diff --git a/packages/evolution/src/sdk/builders/phases/Balance.ts b/packages/evolution/src/sdk/builders/phases/Balance.ts index 52f11435..382e50b9 100644 --- a/packages/evolution/src/sdk/builders/phases/Balance.ts +++ b/packages/evolution/src/sdk/builders/phases/Balance.ts @@ -11,11 +11,11 @@ import { Effect, Ref } from "effect" import * as CoreAssets from "../../../Assets/index.js" -import * as EvaluationStateManager from "../EvaluationStateManager.js" +import * as EvaluationStateManager from "../internal/EvaluationStateManager.js" import { mintToAssets } from "../operations/Mint.js" import { BuildOptionsTag, PhaseContextTag, TransactionBuilderError, TxContext } from "../TransactionBuilder.js" +import { calculateCertificateBalance, calculateProposalDeposits, calculateWithdrawals } from "./Calculations.js" import type { PhaseResult } from "./Phases.js" -import { calculateCertificateBalance, calculateProposalDeposits, calculateWithdrawals } from "./utils.js" /** * Helper: Format assets for logging (BigInt-safe, truncates long unit names) diff --git a/packages/evolution/src/sdk/builders/phases/utils.ts b/packages/evolution/src/sdk/builders/phases/Calculations.ts similarity index 98% rename from packages/evolution/src/sdk/builders/phases/utils.ts rename to packages/evolution/src/sdk/builders/phases/Calculations.ts index a946ba51..578dbbfe 100644 --- a/packages/evolution/src/sdk/builders/phases/utils.ts +++ b/packages/evolution/src/sdk/builders/phases/Calculations.ts @@ -1,7 +1,7 @@ /** * Shared utilities for transaction builder phases * - * @module phases/utils + * @module phases/Calculations * @since 2.0.0 */ @@ -140,7 +140,7 @@ export function calculateProposalDeposits( * * This is used for: * 1. Tracking redeemers by voter in Vote.ts - * 2. Computing vote redeemer indices in TxBuilderImpl.ts (assembly) + * 2. Computing vote redeemer indices in TxAssembly.ts (assembly) * 3. Mapping evaluation results back to voters in Evaluation.ts * * The key format must match the sorting order used by Cardano ledger for diff --git a/packages/evolution/src/sdk/builders/phases/ChangeCreation.ts b/packages/evolution/src/sdk/builders/phases/ChangeCreation.ts index 5ae56e09..c7a32e01 100644 --- a/packages/evolution/src/sdk/builders/phases/ChangeCreation.ts +++ b/packages/evolution/src/sdk/builders/phases/ChangeCreation.ts @@ -14,6 +14,7 @@ import type * as CoreAddress from "../../../Address.js" import * as CoreAssets from "../../../Assets/index.js" import type * as TxOut from "../../../TxOut.js" import * as CoreUTxO from "../../../UTxO.js" +import { calculateMinimumUtxoLovelace, makeTxOutput as txOutputToTransactionOutput } from "../internal/TxOutput.js" import { mintToAssets } from "../operations/Mint.js" import { AvailableUtxosTag, @@ -24,10 +25,9 @@ import { TransactionBuilderError, TxContext } from "../TransactionBuilder.js" -import { calculateMinimumUtxoLovelace, txOutputToTransactionOutput } from "../TxBuilderImpl.js" import * as Unfrack from "../Unfrack.js" +import { calculateCertificateBalance, calculateProposalDeposits, calculateWithdrawals } from "./Calculations.js" import type { PhaseResult } from "./Phases.js" -import { calculateCertificateBalance, calculateProposalDeposits, calculateWithdrawals } from "./utils.js" /** * Helper: Format assets for logging (BigInt-safe, truncates long unit names) @@ -227,7 +227,7 @@ export const executeChangeCreation = (): Effect.Effect< } // Create the sendAll output using the txOutputToTransactionOutput helper - const sendAllOutput = yield* txOutputToTransactionOutput({ + const sendAllOutput = txOutputToTransactionOutput({ address: state.sendAllTo, assets: tentativeLeftover }) @@ -376,7 +376,7 @@ export const executeChangeCreation = (): Effect.Effect< } // Step 6: Single output path - create single change output - const singleOutput = yield* txOutputToTransactionOutput({ + const singleOutput = txOutputToTransactionOutput({ address: changeAddress, assets: tentativeLeftover }) diff --git a/packages/evolution/src/sdk/builders/phases/Collateral.ts b/packages/evolution/src/sdk/builders/phases/Collateral.ts index a73e2c26..394a65fa 100644 --- a/packages/evolution/src/sdk/builders/phases/Collateral.ts +++ b/packages/evolution/src/sdk/builders/phases/Collateral.ts @@ -14,6 +14,7 @@ import { Effect, Ref } from "effect" import * as CoreAssets from "../../../Assets/index.js" import * as TxOut from "../../../TxOut.js" import * as UTxO from "../../../UTxO.js" +import { calculateMinimumUtxoLovelace } from "../internal/TxOutput.js" import { AvailableUtxosTag, BuildOptionsTag, @@ -22,7 +23,6 @@ import { TransactionBuilderError, TxContext } from "../TransactionBuilder.js" -import { calculateMinimumUtxoLovelace } from "../TxBuilderImpl.js" import type { PhaseResult } from "./Phases.js" // ============================================================================ diff --git a/packages/evolution/src/sdk/builders/phases/Evaluation.ts b/packages/evolution/src/sdk/builders/phases/Evaluation.ts index 39055c4c..fcbbe7b9 100644 --- a/packages/evolution/src/sdk/builders/phases/Evaluation.ts +++ b/packages/evolution/src/sdk/builders/phases/Evaluation.ts @@ -16,7 +16,8 @@ import { INT64_MAX } from "../../../Numeric.js" import * as PolicyId from "../../../PolicyId.js" import * as CoreUTxO from "../../../UTxO.js" import type * as Provider from "../../provider/Provider.js" -import * as EvaluationStateManager from "../EvaluationStateManager.js" +import * as EvaluationStateManager from "../internal/EvaluationStateManager.js" +import { assembleTransaction, buildTransactionInputs } from "../internal/TxAssembly.js" import type { IndexedInput } from "../RedeemerBuilder.js" import { BuildOptionsTag, @@ -30,9 +31,8 @@ import { TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" -import { assembleTransaction, buildTransactionInputs } from "../TxBuilderImpl.js" +import { voterToKey } from "./Calculations.js" import type { PhaseResult } from "./Phases.js" -import { voterToKey } from "./utils.js" /** * Convert ProtocolParameters cost models to CostModels core type for evaluation. @@ -325,7 +325,7 @@ export const executeEvaluation = (): Effect.Effect< if ( hasResolvedRedeemers && !hasDeferredRedeemers && - EvaluationStateManager.allRedeemersEvaluated(state.redeemers) + EvaluationStateManager.areAllRedeemersEvaluated(state.redeemers) ) { yield* Effect.logDebug("[Evaluation] All redeemers already evaluated - skipping re-evaluation") return { next: "feeCalculation" as const } @@ -463,7 +463,7 @@ export const executeEvaluation = (): Effect.Effect< } } - const inputs = yield* buildTransactionInputs(sortedUtxos) + const inputs = buildTransactionInputs(sortedUtxos) const allOutputs = [...updatedState.outputs, ...buildCtx.changeOutputs] const transaction = yield* assembleTransaction(inputs, allOutputs, buildCtx.calculatedFee) diff --git a/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts b/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts index 597c6606..7a3b9890 100644 --- a/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts +++ b/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts @@ -11,9 +11,11 @@ import { Effect, Ref } from "effect" import * as CoreAssets from "../../../Assets/index.js" -import type { BuildOptionsTag, TransactionBuilderError } from "../TransactionBuilder.js" -import { PhaseContextTag, ProtocolParametersTag, TxContext } from "../TransactionBuilder.js" -import { buildTransactionInputs, calculateFeeIteratively, calculateReferenceScriptFee } from "../TxBuilderImpl.js" +import { calculateReferenceScriptFee as calculateReferenceScriptFeeCore } from "../../../utils/FeeValidation.js" +import { calculateFeeIteratively } from "../internal/FeeEstimation.js" +import { buildTransactionInputs } from "../internal/TxAssembly.js" +import type { BuildOptionsTag } from "../TransactionBuilder.js" +import { PhaseContextTag, ProtocolParametersTag, TransactionBuilderError, TxContext } from "../TransactionBuilder.js" import type { PhaseResult } from "./Phases.js" /** @@ -67,7 +69,7 @@ export const executeFeeCalculation = (): Effect.Effect< const baseOutputs = state.outputs // Step 2: Build transaction inputs - const inputs = yield* buildTransactionInputs(selectedUtxos) + const inputs = buildTransactionInputs(selectedUtxos) // Step 3: Combine base outputs + change outputs yield* Effect.logDebug( @@ -89,10 +91,10 @@ export const executeFeeCalculation = (): Effect.Effect< // Step 4a: Add reference script fee for scripts on spent inputs and reference inputs const costPerByte = protocolParams.minFeeRefScriptCostPerByte ?? 44 - const refScriptFee = yield* calculateReferenceScriptFee( + const refScriptFee = yield* calculateReferenceScriptFeeCore( [...state.selectedUtxos, ...state.referenceInputs], costPerByte - ) + ).pipe(Effect.mapError((e) => new TransactionBuilderError({ message: e.message, cause: e }))) yield* Effect.logDebug(`[FeeCalculation] Reference script fee: ${refScriptFee}`) const calculatedFee = baseFee + refScriptFee diff --git a/packages/evolution/src/sdk/builders/phases/Selection.ts b/packages/evolution/src/sdk/builders/phases/Selection.ts index 94d1e99b..21995215 100644 --- a/packages/evolution/src/sdk/builders/phases/Selection.ts +++ b/packages/evolution/src/sdk/builders/phases/Selection.ts @@ -14,7 +14,8 @@ import * as CoreAssets from "../../../Assets/index.js" import * as CoreUTxO from "../../../UTxO.js" import type { CoinSelectionAlgorithm, CoinSelectionFunction } from "../CoinSelection.js" import { largestFirstSelection } from "../CoinSelection.js" -import * as EvaluationStateManager from "../EvaluationStateManager.js" +import * as EvaluationStateManager from "../internal/EvaluationStateManager.js" +import { calculateTotalAssets } from "../internal/UtxoAnalysis.js" import { negatedMintAssets } from "../operations/Mint.js" import { AvailableUtxosTag, @@ -23,7 +24,6 @@ import { TransactionBuilderError, TxContext } from "../TransactionBuilder.js" -import { calculateTotalAssets } from "../TxBuilderImpl.js" import type { PhaseResult } from "./Phases.js" /** @@ -363,7 +363,7 @@ export const executeSelection = (): Effect.Effect< // This handles cases where refunds/withdrawals cover all costs, but no UTxO inputs exist const stateAfterSelection = yield* Ref.get(ctx) if (stateAfterSelection.selectedUtxos.length === 0) { - //TODO: double check if this is a good approach, it seems that this condition is only needed when refunds/withdrawals cover all costs + // Cardano requires at least one input even when withdrawals/refunds cover all costs — select a minimal UTxO. const allAvailableUtxos = yield* AvailableUtxosTag const state = yield* Ref.get(ctx) diff --git a/packages/evolution/src/sdk/client/Blockfrost.ts b/packages/evolution/src/sdk/client/Blockfrost.ts new file mode 100644 index 00000000..e155ad0b --- /dev/null +++ b/packages/evolution/src/sdk/client/Blockfrost.ts @@ -0,0 +1,70 @@ +/** + * Blockfrost provider for the composable client API. + * + * Adds query, submission, evaluation, and await capabilities. + * + * @example + * ```ts + * import { client, preview, blockfrost } from "@evolution-sdk/evolution" + * + * const myClient = client(preview) + * .with(blockfrost({ baseUrl: "https://cardano-preview.blockfrost.io/api/v0", projectId: "..." })) + * ``` + * + * @since 2.1.0 + * @module + */ + +import * as BlockfrostEffect from "../provider/internal/BlockfrostEffect.js" +import { attachCapabilities } from "./attachCapabilities.js" +import type { BlockfrostCapabilities } from "./Capabilities.js" +import { type Client } from "./Client.js" + +// ── Configuration ───────────────────────────────────────────────────────────── + +/** + * Configuration for the Blockfrost provider. + * + * @since 2.1.0 + * @category model + */ +export interface BlockfrostConfig { + readonly baseUrl: string + readonly projectId?: string +} + +// ── Constructor ─────────────────────────────────────────────────────────────── + +/** + * Blockfrost provider constructor. + * + * Adds query, submission, evaluation, and await capabilities. + * + * @example + * ```ts + * import { client, preview, blockfrost } from "@evolution-sdk/evolution" + * + * const myClient = client(preview) + * .with(blockfrost({ baseUrl: "https://cardano-preview.blockfrost.io/api/v0", projectId: "..." })) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const blockfrost = (cfg: BlockfrostConfig) => + ( + c: T + ): T & BlockfrostCapabilities => { + return attachCapabilities(c, { + getUtxos: BlockfrostEffect.getUtxos(cfg.baseUrl, cfg.projectId), + getUtxosByOutRef: BlockfrostEffect.getUtxosByOutRef(cfg.baseUrl, cfg.projectId), + getUtxosWithUnit: BlockfrostEffect.getUtxosWithUnit(cfg.baseUrl, cfg.projectId), + getUtxoByUnit: BlockfrostEffect.getUtxoByUnit(cfg.baseUrl, cfg.projectId), + getProtocolParameters: () => BlockfrostEffect.getProtocolParameters(cfg.baseUrl, cfg.projectId), + getDelegation: BlockfrostEffect.getDelegation(cfg.baseUrl, cfg.projectId), + submitTx: BlockfrostEffect.submitTx(cfg.baseUrl, cfg.projectId), + awaitTx: BlockfrostEffect.awaitTx(cfg.baseUrl, cfg.projectId), + getDatum: BlockfrostEffect.getDatum(cfg.baseUrl, cfg.projectId), + evaluateTx: BlockfrostEffect.evaluateTx(cfg.baseUrl, cfg.projectId) + }) + } diff --git a/packages/evolution/src/sdk/client/Capabilities.ts b/packages/evolution/src/sdk/client/Capabilities.ts new file mode 100644 index 00000000..a10b78e0 --- /dev/null +++ b/packages/evolution/src/sdk/client/Capabilities.ts @@ -0,0 +1,442 @@ +/** + * Fine-grained capability interfaces for the composable client API. + * + * Each interface represents a single, composable capability that carries both + * a Promise method and its Effect counterpart under the `Effect` namespace. + * TypeScript's intersection merging automatically combines `Effect` properties + * across capabilities. + * + * @example + * ```ts + * import type { QueryUtxos, Addressable } from "@evolution-sdk/evolution" + * + * const getBalance = async (client: QueryUtxos & Addressable) => { + * // Promise API + * const utxos = await client.getUtxos(await client.getAddress()) + * // Effect API + * client.Effect.getUtxos(addr) + * } + * ``` + * + * @since 2.1.0 + * @module + */ + +import type { Effect, Stream } from "effect" + +import type * as CoreAddress from "../../Address.js" +import type * as Credential from "../../Credential.js" +import type * as PlutusData from "../../Data.js" +import type * as DatumHash from "../../DatumHash.js" +import type * as RewardAddress from "../../RewardAddress.js" +import type * as Script from "../../Script.js" +import type * as ScriptHash from "../../ScriptHash.js" +import type * as Transaction from "../../Transaction.js" +import type * as TransactionHash from "../../TransactionHash.js" +import type * as TransactionInput from "../../TransactionInput.js" +import type * as TransactionWitnessSet from "../../TransactionWitnessSet.js" +import type * as CoreUTxO from "../../UTxO.js" +import type { EvalRedeemer } from "../EvalRedeemer.js" +import type { Delegation, ProtocolParameters, ProviderError } from "../provider/Provider.js" +import type { Payload, SignedMessage, WalletError } from "../wallet/Wallet.js" + +// ── Provider Capabilities ───────────────────────────────────────────────────── + +/** + * Query UTxOs at an address filtered by a specific asset unit. + * + * @since 2.1.0 + * @category capabilities + */ +export interface QueryUtxosWithUnit { + readonly getUtxosWithUnit: ( + addressOrCredential: CoreAddress.Address | Credential.Credential, + unit: string + ) => Promise> + readonly Effect: { + readonly getUtxosWithUnit: ( + addressOrCredential: CoreAddress.Address | Credential.Credential, + unit: string + ) => Effect.Effect, ProviderError> + } +} + +/** + * Query a single UTxO by its asset unit (returns the UTxO holding that unit). + * + * @since 2.1.0 + * @category capabilities + */ +export interface QueryUtxoByUnit { + readonly getUtxoByUnit: (unit: string) => Promise + readonly Effect: { + readonly getUtxoByUnit: (unit: string) => Effect.Effect + } +} + +/** + * Query delegation for the wallet's own reward address. + * + * @since 2.1.0 + * @category capabilities + */ +export interface WalletDelegation { + readonly getWalletDelegation: () => Promise + readonly Effect: { + readonly getWalletDelegation: () => Effect.Effect + } +} + +/** + * Query UTxOs at an address or by credential. + * + * @since 2.1.0 + * @category capabilities + */ +export interface QueryUtxos { + readonly getUtxos: ( + addressOrCredential: CoreAddress.Address | Credential.Credential + ) => Promise> + readonly Effect: { + readonly getUtxos: ( + addressOrCredential: CoreAddress.Address | Credential.Credential + ) => Effect.Effect, ProviderError> + } +} + +/** + * Query UTxOs by their output references (transaction inputs). + * + * @since 2.1.0 + * @category capabilities + */ +export interface QueryUtxosByOutRef { + readonly getUtxosByOutRef: ( + inputs: ReadonlyArray + ) => Promise> + readonly Effect: { + readonly getUtxosByOutRef: ( + inputs: ReadonlyArray + ) => Effect.Effect, ProviderError> + } +} + +/** + * Query current protocol parameters. + * + * @since 2.1.0 + * @category capabilities + */ +export interface QueryProtocolParams { + readonly getProtocolParameters: () => Promise + readonly Effect: { + readonly getProtocolParameters: () => Effect.Effect + } +} + +/** + * Submit a signed transaction. + * + * @since 2.1.0 + * @category capabilities + */ +export interface SubmitTx { + readonly submitTx: (tx: Transaction.Transaction) => Promise + readonly Effect: { + readonly submitTx: (tx: Transaction.Transaction) => Effect.Effect + } +} + +/** + * Evaluate a transaction to determine script execution costs. + * + * @since 2.1.0 + * @category capabilities + */ +export interface EvaluateTx { + readonly evaluateTx: ( + tx: Transaction.Transaction, + additionalUTxOs?: Array + ) => Promise> + readonly Effect: { + readonly evaluateTx: ( + tx: Transaction.Transaction, + additionalUTxOs?: Array + ) => Effect.Effect, ProviderError> + } +} + +/** + * Query delegation info for a reward address. + * + * @since 2.1.0 + * @category capabilities + */ +export interface QueryDelegation { + readonly getDelegation: (rewardAddress: RewardAddress.RewardAddress) => Promise + readonly Effect: { + readonly getDelegation: (rewardAddress: RewardAddress.RewardAddress) => Effect.Effect + } +} + +/** + * Wait for a transaction to be confirmed on-chain. + * + * @since 2.1.0 + * @category capabilities + */ +export interface AwaitTx { + readonly awaitTx: ( + txHash: TransactionHash.TransactionHash, + checkInterval?: number, + timeout?: number + ) => Promise + readonly Effect: { + readonly awaitTx: ( + txHash: TransactionHash.TransactionHash, + checkInterval?: number, + timeout?: number + ) => Effect.Effect + } +} + +/** + * Query a datum by its hash. + * + * @since 2.1.0 + * @category capabilities + */ +export interface QueryDatumByHash { + readonly getDatum: (datumHash: DatumHash.DatumHash) => Promise + readonly Effect: { + readonly getDatum: (datumHash: DatumHash.DatumHash) => Effect.Effect + } +} + +/** + * Query a script by its hash. + * + * @since 2.1.0 + * @category capabilities + */ +export interface QueryScript { + readonly getScript: (hash: ScriptHash.ScriptHash) => Promise + readonly Effect: { + readonly getScript: (hash: ScriptHash.ScriptHash) => Effect.Effect + } +} + +/** + * Watch UTxOs at an address, emitting new UTxOs as they appear on-chain. + * + * Promise side returns an `AsyncIterable` — use `for await` to consume. + * `break` triggers cleanup automatically. + * + * @since 2.2.0 + * @category capabilities + */ +export interface WatchUtxos { + readonly watchUtxos: ( + addressOrCredential: CoreAddress.Address | Credential.Credential, + pollInterval?: number + ) => AsyncIterable + readonly Effect: { + readonly watchUtxos: ( + addressOrCredential: CoreAddress.Address | Credential.Credential, + pollInterval?: number + ) => Stream.Stream + } +} + +// ── Wallet Capabilities ─────────────────────────────────────────────────────── + +/** + * Get the wallet's primary address. + * + * @since 2.1.0 + * @category capabilities + */ +export interface Addressable { + readonly getAddress: () => Promise + readonly Effect: { + readonly getAddress: () => Effect.Effect + } +} + +/** + * Get the wallet's reward (stake) address. + * + * @since 2.1.0 + * @category capabilities + */ +export interface Stakeable { + readonly getRewardAddress: () => Promise + readonly Effect: { + readonly getRewardAddress: () => Effect.Effect + } +} + +/** + * Sign a transaction. + * + * @since 2.1.0 + * @category capabilities + */ +export interface Signable { + readonly signTx: ( + tx: Transaction.Transaction | string, + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } + ) => Promise + readonly Effect: { + readonly signTx: ( + tx: Transaction.Transaction | string, + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } + ) => Effect.Effect + } +} + +/** + * Sign an arbitrary message with a wallet key. + * + * @since 2.1.0 + * @category capabilities + */ +export interface SignData { + readonly signMessage: ( + address: CoreAddress.Address | RewardAddress.RewardAddress, + payload: Payload + ) => Promise + readonly Effect: { + readonly signMessage: ( + address: CoreAddress.Address | RewardAddress.RewardAddress, + payload: Payload + ) => Effect.Effect + } +} + +/** + * Query UTxOs from the wallet itself (CIP-30). + * + * @since 2.1.0 + * @category capabilities + */ +export interface WalletUtxos { + readonly getWalletUtxos: () => Promise> + readonly Effect: { + readonly getWalletUtxos: () => Effect.Effect, WalletError | ProviderError> + } +} + +/** + * Get collateral UTxOs from the wallet (CIP-30). + * + * @since 2.1.0 + * @category capabilities + */ +export interface CollateralSource { + readonly getCollateral: () => Promise> + readonly Effect: { + readonly getCollateral: () => Effect.Effect, WalletError> + } +} + +/** + * Submit a transaction through the wallet API (CIP-30). + * + * @since 2.1.0 + * @category capabilities + */ +export interface WalletSubmit { + readonly walletSubmitTx: (tx: Transaction.Transaction | string) => Promise + readonly Effect: { + readonly walletSubmitTx: ( + tx: Transaction.Transaction | string + ) => Effect.Effect + } +} + +/** + * Derive addresses from an HD wallet at arbitrary roles and indices. + * + * @since 2.1.0 + * @category capabilities + */ +export interface Derivable { + readonly deriveAddress: (role: number, index: number) => Promise + readonly Effect: { + readonly deriveAddress: (role: number, index: number) => Effect.Effect + } +} + +// ── Aggregate types for convenience ─────────────────────────────────────────── + +/** + * All capabilities provided by a full provider. + * + * @since 2.1.0 + * @category capabilities + */ +export type FullProviderCapabilities = QueryUtxos & + QueryUtxosByOutRef & + QueryUtxosWithUnit & + QueryUtxoByUnit & + QueryProtocolParams & + QueryDelegation & + SubmitTx & + EvaluateTx & + AwaitTx & + QueryDatumByHash + +// ── Per-provider capability types ───────────────────────────────────────────── +// +// Each provider gets its own named type so it can diverge independently. +// Today they all equal FullProviderCapabilities. When a provider gains or +// loses a capability, change that provider's type — no others are affected. + +/** + * Capabilities provided by the Blockfrost provider. + * + * @since 2.1.0 + * @category capabilities + */ +export type BlockfrostCapabilities = FullProviderCapabilities + +/** + * Capabilities provided by the Maestro provider. + * + * @since 2.1.0 + * @category capabilities + */ +export type MaestroCapabilities = FullProviderCapabilities + +/** + * Capabilities provided by the Koios provider. + * + * @since 2.1.0 + * @category capabilities + */ +export type KoiosCapabilities = FullProviderCapabilities + +/** + * Capabilities provided by the Kupmios (Kupo + Ogmios) provider. + * + * @since 2.1.0 + * @category capabilities + */ +export type KupmiosCapabilities = FullProviderCapabilities & WatchUtxos + +/** + * All capabilities provided by a signing wallet. + * + * @since 2.1.0 + * @category capabilities + */ +export type SigningWalletCapabilities = Addressable & Signable & SignData & Stakeable + +/** + * All capabilities provided by a CIP-30 browser wallet. + * + * @since 2.1.0 + * @category capabilities + */ +export type Cip30WalletCapabilities = Addressable & Signable & SignData & Stakeable & WalletSubmit diff --git a/packages/evolution/src/sdk/client/Chain.ts b/packages/evolution/src/sdk/client/Chain.ts new file mode 100644 index 00000000..4c2039e8 --- /dev/null +++ b/packages/evolution/src/sdk/client/Chain.ts @@ -0,0 +1,134 @@ +/** + * Cardano chain descriptors for client configuration. + * + * A `Chain` is a complete, self-describing network descriptor. It carries all + * the information needed to configure addresses, slot arithmetic, and block + * explorer URLs for a given Cardano network. + * + * Use the built-in constants for known networks, or `defineChain` for custom + * devnets and private networks. + * + * @example + * import { preprod, mainnet, preview, defineChain } from "@evolution-sdk/evolution" + * + * @since 2.1.0 + * @category model + */ +export interface Chain { + /** Human-readable network name */ + readonly name: string + /** + * CBOR network encoding. + * `1` = mainnet, `0` = all testnets. + */ + readonly id: 0 | 1 + /** + * Protocol magic number — uniquely identifies the network instance. + * Mainnet: 764824073 | Preprod: 1 | Preview: 2 | Custom: any + */ + readonly networkMagic: number + /** Slot configuration for time ↔ slot conversion */ + readonly slotConfig: { + /** Unix timestamp (milliseconds) of the Shelley era start */ + readonly zeroTime: bigint + /** First slot number of the Shelley era */ + readonly zeroSlot: bigint + /** Duration of each slot in milliseconds (typically 1000) */ + readonly slotLength: number + } + /** Number of slots per epoch */ + readonly epochLength: number + /** Block explorer base URLs (optional — not available for custom chains) */ + readonly blockExplorers?: { + readonly cardanoscan?: string + readonly cexplorer?: string + } +} + +/** + * Define a custom Cardano chain for devnets, private networks, or any network + * not built into the SDK. + * + * @example + * import { defineChain, client, kupmios } from "@evolution-sdk/evolution" + * + * const devnet = defineChain({ + * name: "Local Devnet", + * id: 0, + * networkMagic: 42, + * slotConfig: { zeroTime: 1743379200000n, zeroSlot: 0n, slotLength: 1000 }, + * epochLength: 500, + * }) + * + * const c = client(devnet).with(kupmios({ kupoUrl: "http://localhost:1442", ogmiosUrl: "ws://localhost:1337" })) + * + * @since 2.1.0 + * @category constructors + */ +export const defineChain = (chain: Chain): Chain => chain + +/** + * Cardano Mainnet. + * + * @since 2.1.0 + * @category chains + */ +export const mainnet: Chain = { + name: "Cardano Mainnet", + id: 1, + networkMagic: 764824073, + slotConfig: { + zeroTime: 1596059091000n, + zeroSlot: 4492800n, + slotLength: 1000 + }, + epochLength: 432000, + blockExplorers: { + cardanoscan: "https://cardanoscan.io", + cexplorer: "https://cexplorer.io" + } +} + +/** + * Cardano Pre-Production Testnet (Preprod). + * + * @since 2.1.0 + * @category chains + */ +export const preprod: Chain = { + name: "Cardano Preprod", + id: 0, + networkMagic: 1, + slotConfig: { + zeroTime: 1655769600000n, + zeroSlot: 86400n, + slotLength: 1000 + }, + epochLength: 432000, + blockExplorers: { + cardanoscan: "https://preprod.cardanoscan.io", + cexplorer: "https://preprod.cexplorer.io" + } +} + +/** + * Cardano Preview Testnet. + * + * @since 2.1.0 + * @category chains + */ +export const preview: Chain = { + name: "Cardano Preview", + id: 0, + networkMagic: 2, + slotConfig: { + zeroTime: 1666656000000n, + zeroSlot: 0n, + slotLength: 1000 + }, + epochLength: 86400, + blockExplorers: { + cardanoscan: "https://preview.cardanoscan.io", + cexplorer: "https://preview.cexplorer.io" + } +} diff --git a/packages/evolution/src/sdk/client/Client.ts b/packages/evolution/src/sdk/client/Client.ts index 48f9fbe1..dac1392e 100644 --- a/packages/evolution/src/sdk/client/Client.ts +++ b/packages/evolution/src/sdk/client/Client.ts @@ -1,335 +1,185 @@ -import { Data, type Effect, type Schedule } from "effect" - -import type * as CoreUTxO from "../../UTxO.js" -import type { ReadOnlyTransactionBuilder, SigningTransactionBuilder } from "../builders/TransactionBuilder.js" -import type * as Provider from "../provider/Provider.js" -import type { EffectToPromiseAPI } from "../Type.js" -import type { - ApiWalletEffect, - ReadOnlyWalletEffect, - SigningWalletEffect, - WalletApi, - WalletError -} from "../wallet/WalletNew.js" - /** - * Error class for provider-related operations. + * Composable client API. * - * @since 2.0.0 - * @category errors - */ -export class ProviderError extends Data.TaggedError("ProviderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * MinimalClient Effect - holds network context. + * Build clients by calling `.with()` on a base `client(chain)` with provider + * and wallet constructors. Each constructor adds capabilities to the client via + * intersection types, and TypeScript infers the accumulated type automatically. * - * @since 2.0.0 - * @category model - */ -export interface MinimalClientEffect { - readonly networkId: Effect.Effect -} - -/** - * ReadOnlyClient Effect - provider, read-only wallet, and utility methods. + * @example + * ```ts + * import { client, preview, blockfrost, seedWallet } from "@evolution-sdk/evolution" * - * @since 2.0.0 - * @category model - */ -export interface ReadOnlyClientEffect extends Provider.ProviderEffect, ReadOnlyWalletEffect { - readonly getWalletUtxos: () => Effect.Effect, Provider.ProviderError> - readonly getWalletDelegation: () => Effect.Effect -} - -/** - * SigningClient Effect - provider, signing wallet, and utility methods. + * const myClient = client(preview) + * .with(blockfrost({ baseUrl: "...", projectId: "..." })) + * .with(seedWallet({ mnemonic: "..." })) * - * @since 2.0.0 - * @category model - */ -export interface SigningClientEffect extends Provider.ProviderEffect, SigningWalletEffect { - readonly getWalletUtxos: () => Effect.Effect, WalletError | Provider.ProviderError> - readonly getWalletDelegation: () => Effect.Effect -} - -/** - * MinimalClient - network context with combinator methods to attach provider and/or wallet. + * // Promise API + * await myClient.getUtxos(addr) + * await myClient.signTx(tx) * - * @since 2.0.0 - * @category model - */ -export interface MinimalClient { - readonly networkId: number | string - readonly attachProvider: (config: ProviderConfig) => ProviderOnlyClient - readonly attachWallet: ( - config: T - ) => T extends SeedWalletConfig - ? SigningWalletClient - : T extends PrivateKeyWalletConfig - ? SigningWalletClient - : T extends ApiWalletConfig - ? ApiWalletClient - : ReadOnlyWalletClient - readonly attach: ( - providerConfig: ProviderConfig, - walletConfig: TW - ) => TW extends SeedWalletConfig - ? SigningClient - : TW extends PrivateKeyWalletConfig - ? SigningClient - : TW extends ApiWalletConfig - ? SigningClient - : ReadOnlyClient - readonly Effect: MinimalClientEffect -} - -/** - * ProviderOnlyClient - blockchain queries and transaction submission. + * // Transaction building: + * myClient.newTx().payToAddress({ address: "addr1...", assets: { lovelace: 5_000_000n } }) * - * @since 2.0.0 - * @category model - */ -export type ProviderOnlyClient = EffectToPromiseAPI & { - readonly attachWallet: ( - config: T - ) => T extends SeedWalletConfig - ? SigningClient - : T extends PrivateKeyWalletConfig - ? SigningClient - : T extends ApiWalletConfig - ? SigningClient - : ReadOnlyClient - readonly Effect: Provider.ProviderEffect -} - -/** - * ReadOnlyClient - blockchain queries and wallet address operations without signing. - * Use newTx() to build unsigned transactions. + * // Effect API + * myClient.Effect.getUtxos(addr).pipe(Effect.flatMap(...)) + * ``` * - * @since 2.0.0 - * @category model + * @since 2.1.0 + * @module */ -export type ReadOnlyClient = EffectToPromiseAPI & { - readonly newTx: (utxos?: ReadonlyArray) => ReadOnlyTransactionBuilder - readonly Effect: ReadOnlyClientEffect -} -/** - * SigningClient - full functionality: blockchain queries, transaction signing, and submission. - * Use newTx() to build, sign, and submit transactions. - * - * @since 2.0.0 - * @category model - */ -export type SigningClient = EffectToPromiseAPI & { - readonly newTx: () => SigningTransactionBuilder - readonly Effect: SigningClientEffect -} +import { Effect } from "effect" -/** - * ApiWalletClient - CIP-30 wallet signing and submission without blockchain queries. - * Requires attachProvider() to access blockchain data. - * - * @since 2.0.0 - * @category model - */ -export type ApiWalletClient = EffectToPromiseAPI & { - readonly attachProvider: (config: ProviderConfig) => SigningClient - readonly Effect: ApiWalletEffect -} +import { + makeCapabilityTxBuilder, + type ProviderLike, + type ReadOnlyWalletLike, + type SigningWalletLike, + type TxBuilder, + type WalletLike, +} from "../builders/TransactionBuilder.js" +import type { Chain } from "./Chain.js" -/** - * SigningWalletClient - transaction signing without blockchain queries. - * Requires attachProvider() to access blockchain data. - * - * @since 2.0.0 - * @category model - */ -export type SigningWalletClient = EffectToPromiseAPI & { - readonly networkId: number | string - readonly attachProvider: (config: ProviderConfig) => SigningClient - readonly Effect: SigningWalletEffect -} +// Re-export provider constructors for backward compatibility +export { + blockfrost, + type BlockfrostConfig, +} from "./Blockfrost.js" +export { + koios, + type KoiosConfig, +} from "./Koios.js" +export { + kupmios, + type KupmiosConfig, +} from "./Kupmios.js" +export { + maestro, + type MaestroConfig, +} from "./Maestro.js" -/** - * ReadOnlyWalletClient - wallet address access without signing or blockchain queries. - * Requires attachProvider() to access blockchain data. - * - * @since 2.0.0 - * @category model - */ -export type ReadOnlyWalletClient = EffectToPromiseAPI & { - readonly networkId: number | string - readonly attachProvider: (config: ProviderConfig) => ReadOnlyClient - readonly Effect: ReadOnlyWalletEffect -} +// Re-export wallet constructors for backward compatibility +export { + cip30Wallet, + privateKeyWallet, + type PrivateKeyWalletConfig, + readOnlyWallet, + seedWallet, + type SeedWalletConfig, +} from "./Wallets.js" -/** - * Network identifier for client configuration. - * - * @since 2.0.0 - * @category model - */ -export type NetworkId = "mainnet" | "preprod" | "preview" | number +// ── Client ──────────────────────────────────────────────────────────────────── /** - * Retry policy configuration with exponential backoff. + * Base client carrying chain context. All composable clients extend this. * - * @since 2.0.0 + * @since 2.1.0 * @category model */ -export interface RetryConfig { - readonly maxRetries: number - readonly retryDelayMs: number - readonly backoffMultiplier: number - readonly maxRetryDelayMs: number +export interface Client { + readonly chain: C + readonly networkId: C["id"] + readonly Effect: {} + readonly newTx: () => TxBuilder + readonly with: (fn: (c: this) => R) => R } /** - * Preset retry configurations for common scenarios. - * - * @since 2.0.0 - * @category constants - */ -export const RetryPresets = { - none: { maxRetries: 0, retryDelayMs: 0, backoffMultiplier: 1, maxRetryDelayMs: 0 } as const, - fast: { maxRetries: 3, retryDelayMs: 500, backoffMultiplier: 1.5, maxRetryDelayMs: 5000 } as const, - standard: { maxRetries: 3, retryDelayMs: 1000, backoffMultiplier: 2, maxRetryDelayMs: 10000 } as const, - aggressive: { maxRetries: 5, retryDelayMs: 1000, backoffMultiplier: 2, maxRetryDelayMs: 30000 } as const -} as const - -/** - * Retry policy - preset config, custom schedule, or preset reference. + * Create a base client from a chain descriptor. * - * @since 2.0.0 - * @category model - */ -export type RetryPolicy = RetryConfig | Schedule.Schedule | { preset: keyof typeof RetryPresets } - -/** - * Blockfrost provider configuration. + * @example + * ```ts + * import { client, preview, blockfrost } from "@evolution-sdk/evolution" * - * @since 2.0.0 - * @category model - */ -export interface BlockfrostConfig { - readonly type: "blockfrost" - readonly baseUrl: string - readonly projectId?: string - readonly retryPolicy?: RetryPolicy -} - -/** - * Kupmios provider configuration (Kupo + Ogmios). + * const myClient = client(preview) + * .with(blockfrost({ baseUrl: "...", projectId: "..." })) + * ``` * - * @since 2.0.0 - * @category model + * @since 2.1.0 + * @category constructors */ -export interface KupmiosConfig { - readonly type: "kupmios" - readonly kupoUrl: string - readonly ogmiosUrl: string - readonly headers?: { - readonly ogmiosHeader?: Record - readonly kupoHeader?: Record +export const client = (chain: C): Client => { + const result: Client = { + chain, + networkId: chain.id, + Effect: {}, + newTx: () => newTx(result), + with: (fn: (c: Client) => R): R => fn(result) } - readonly retryPolicy?: RetryPolicy -} - -/** - * Maestro provider configuration. - * - * @since 2.0.0 - * @category model - */ -export interface MaestroConfig { - readonly type: "maestro" - readonly baseUrl: string - readonly apiKey: string - readonly turboSubmit?: boolean - readonly retryPolicy?: RetryPolicy -} - -/** - * Koios provider configuration. - * - * @since 2.0.0 - * @category model - */ -export interface KoiosConfig { - readonly type: "koios" - readonly baseUrl: string - readonly token?: string - readonly retryPolicy?: RetryPolicy -} - -/** - * Provider configuration union type. - * - * @since 2.0.0 - * @category model - */ -export type ProviderConfig = BlockfrostConfig | KupmiosConfig | MaestroConfig | KoiosConfig - -/** - * Seed phrase wallet configuration. - * - * @since 2.0.0 - * @category model - */ -export interface SeedWalletConfig { - readonly type: "seed" - readonly mnemonic: string - readonly accountIndex?: number - readonly paymentIndex?: number - readonly stakeIndex?: number - readonly addressType?: "Base" | "Enterprise" - readonly password?: string + return result +} + +// ── Transaction builder ─────────────────────────────────────────────────────── + +/** + * Create a TxBuilder from a composable client. + * + * Extracts provider and wallet capabilities from the client's Effect namespace + * and maps them to the builder's expected interfaces using structural typing. + * + * The return type `TxBuilder` carries the client's full capability set. + * At build time, `BuildArgs` computes which `BuildOptions` fields are required + * based on what T can provide automatically: + * - `protocolParameters` required unless T extends `QueryProtocolParams` + * - `changeAddress` required unless T extends `Addressable` + * - `availableUtxos` required unless T extends `QueryUtxos` + * - `evaluator` required if R has accumulated `EvaluateTxCapability` AND T doesn't have it + * + * `build().then(sb => sb.sign())` is only available when T extends `Signable`. + * + * @example + * ```ts + * import { client, preview, blockfrost, seedWallet, newTx } from "@evolution-sdk/evolution" + * + * const myClient = client(preview) + * .with(blockfrost({ baseUrl: "...", projectId: "..." })) + * .with(seedWallet({ mnemonic: "..." })) + * + * const tx = newTx(myClient) + * .payToAddress({ address: "addr1...", assets: { lovelace: 5_000_000n } }) + * + * const signed = await tx.build() + * .then(sb => sb.sign()) + * .then(sb => sb.submit()) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const newTx = (client: T): TxBuilder => { + const eff = client.Effect as Record + + // Build ProviderLike if client has provider capabilities + const hasProvider = typeof eff.getProtocolParameters === "function" + const provider: ProviderLike | undefined = hasProvider ? ({ Effect: eff } as ProviderLike) : undefined + + // Build WalletLike if client has wallet capabilities + const hasAddress = typeof eff.getAddress === "function" + const hasSign = typeof eff.signTx === "function" + const rewardAddress: ReadOnlyWalletLike["Effect"]["rewardAddress"] = + (eff.getRewardAddress as ReadOnlyWalletLike["Effect"]["rewardAddress"] | undefined) ?? + (() => Effect.succeed(null)) + + const wallet: WalletLike | undefined = hasAddress + ? hasSign + ? { + Effect: { + address: eff.getAddress as ReadOnlyWalletLike["Effect"]["address"], + rewardAddress, + signTx: eff.signTx as SigningWalletLike["Effect"]["signTx"], + }, + } + : { + Effect: { + address: eff.getAddress as ReadOnlyWalletLike["Effect"]["address"], + rewardAddress, + }, + } + : undefined + + return makeCapabilityTxBuilder({ + wallet, + provider, + slotConfig: client.chain.slotConfig, + }) } - -/** - * Private key wallet configuration. - * - * @since 2.0.0 - * @category model - */ -export interface PrivateKeyWalletConfig { - readonly type: "private-key" - readonly paymentKey: string - readonly stakeKey?: string - readonly addressType?: "Base" | "Enterprise" -} - -/** - * Read-only wallet configuration. - * - * @since 2.0.0 - * @category model - */ -export interface ReadOnlyWalletConfig { - readonly type: "read-only" - readonly address: string - readonly rewardAddress?: string -} - -/** - * CIP-30 API wallet configuration. - * - * @since 2.0.0 - * @category model - */ -export interface ApiWalletConfig { - readonly type: "api" - readonly api: WalletApi -} - -/** - * Wallet configuration union type. - * - * @since 2.0.0 - * @category model - */ -export type WalletConfig = SeedWalletConfig | PrivateKeyWalletConfig | ReadOnlyWalletConfig | ApiWalletConfig diff --git a/packages/evolution/src/sdk/client/ClientImpl.ts b/packages/evolution/src/sdk/client/ClientImpl.ts index c446ebb7..481ae43c 100644 --- a/packages/evolution/src/sdk/client/ClientImpl.ts +++ b/packages/evolution/src/sdk/client/ClientImpl.ts @@ -1,21 +1,22 @@ -import { Effect, Equal, ParseResult, Schema } from "effect" +/** + * Legacy client implementation. + * + * This module implements the original `createClient(config)` factory pattern. + * New code should use the composable `client(chain).with(provider).with(wallet)` + * API exported from `Client.ts` instead. + * + * @deprecated Prefer the composable client API in `Client.ts`. + * @module + */ + +import { Effect, ParseResult, Schema } from "effect" import * as CoreAddress from "../../Address.js" -import * as Bytes from "../../Bytes.js" -import * as KeyHash from "../../KeyHash.js" -import type * as NativeScripts from "../../NativeScripts.js" -import type * as Network from "../../Network.js" -import * as PrivateKey from "../../PrivateKey.js" -import * as CoreRewardAccount from "../../RewardAccount.js" import * as CoreRewardAddress from "../../RewardAddress.js" -import type * as Time from "../../Time/index.js" import * as Transaction from "../../Transaction.js" -import * as TransactionHash from "../../TransactionHash.js" -import * as TransactionWitnessSet from "../../TransactionWitnessSet.js" +import type * as TransactionWitnessSet from "../../TransactionWitnessSet.js" import { runEffectPromise } from "../../utils/effect-runtime.js" -import { hashTransaction, hashTransactionRaw } from "../../utils/Hash.js" -import * as CoreUTxO from "../../UTxO.js" -import * as VKey from "../../VKey.js" +import type * as CoreUTxO from "../../UTxO.js" import { makeTxBuilder, type ReadOnlyTransactionBuilder, @@ -26,14 +27,13 @@ import * as Koios from "../provider/Koios.js" import * as Kupmios from "../provider/Kupmios.js" import * as Maestro from "../provider/Maestro.js" import * as Provider from "../provider/Provider.js" -import * as Derivation from "../wallet/Derivation.js" -import * as WalletNew from "../wallet/WalletNew.js" +import * as Wallet from "../wallet/Wallet.js" +import type { Chain } from "./Chain.js" import { type ApiWalletClient, type ApiWalletConfig, type MinimalClient, type MinimalClientEffect, - type NetworkId, type PrivateKeyWalletConfig, type ProviderConfig, type ProviderOnlyClient, @@ -44,7 +44,7 @@ import { type SigningClient, type SigningWalletClient, type WalletConfig -} from "./Client.js" +} from "./ClientLegacy.js" /** * Create a provider instance from configuration. @@ -61,99 +61,25 @@ const createProvider = (config: ProviderConfig): Provider.Provider => { case "maestro": return new Maestro.MaestroProvider(config.baseUrl, config.apiKey, config.turboSubmit) case "koios": - return new Koios.Koios(config.baseUrl, config.token) + return new Koios.KoiosProvider(config.baseUrl, config.token) } } /** - * Map NetworkId to numeric representation. - * "mainnet" → 1, "preprod"/"preview" → 0, numeric values pass through unchanged. - * - * @since 2.0.0 - * @category transformation - */ -const normalizeNetworkId = (network: NetworkId): number => { - if (typeof network === "number") return network - switch (network) { - case "mainnet": - return 1 - case "preprod": - return 0 - case "preview": - return 0 - default: - return 0 - } -} - -/** - * Map NetworkId to wallet network enumeration. - * Returns "Mainnet" for numeric 1 or string "mainnet"; returns "Testnet" otherwise. - * - * @since 2.0.0 - * @category transformation - */ -const toWalletNetwork = (networkId: NetworkId): WalletNew.Network => { - if (typeof networkId === "number") { - return networkId === 1 ? "Mainnet" : "Testnet" - } - switch (networkId) { - case "mainnet": - return "Mainnet" - case "preprod": - case "preview": - return "Testnet" - default: - return "Testnet" - } -} - -/** - * Map NetworkId to Network type for slot configuration resolution. - * Returns the correct Network variant so resolveSlotConfig picks the right preset. - * - * @since 2.0.0 - * @category transformation - */ -const toBuilderNetwork = (networkId: NetworkId): Network.Network => { - if (typeof networkId === "number") { - return networkId === 1 ? "Mainnet" : "Preview" - } - switch (networkId) { - case "mainnet": - return "Mainnet" - case "preprod": - return "Preprod" - case "preview": - return "Preview" - default: - return "Mainnet" - } -} - -/** - * Construct read-only wallet from network, payment address, and optional reward address. + * Construct read-only wallet from a payment address and optional reward address. * * @since 2.0.0 * @category constructors */ -const createReadOnlyWallet = ( - network: WalletNew.Network, - address: string, - rewardAddress?: string -): WalletNew.ReadOnlyWallet => { +const createReadOnlyWallet = (address: string, rewardAddress?: string): Wallet.ReadOnlyWallet => { const coreAddress = CoreAddress.fromBech32(address) const coreRewardAddress = rewardAddress ? Schema.decodeSync(CoreRewardAddress.RewardAddress)(rewardAddress) : null - const walletEffect: WalletNew.ReadOnlyWalletEffect = { - address: () => Effect.succeed(coreAddress), - rewardAddress: () => Effect.succeed(coreRewardAddress) - } - + const effects = Wallet.makeReadOnlyWalletEffect(coreAddress, coreRewardAddress) return { - address: () => Promise.resolve(coreAddress), - rewardAddress: () => Promise.resolve(coreRewardAddress), - Effect: walletEffect, - type: "read-only" + type: "read-only", + address: () => runEffectPromise(effects.address()), + rewardAddress: () => runEffectPromise(effects.rewardAddress()), + Effect: effects } } @@ -163,22 +89,16 @@ const createReadOnlyWallet = ( * @since 2.0.0 * @category constructors */ -const createReadOnlyWalletClient = (network: NetworkId, config: ReadOnlyWalletConfig): ReadOnlyWalletClient => { - const walletNetwork = toWalletNetwork(network) - const wallet = createReadOnlyWallet(walletNetwork, config.address, config.rewardAddress) - const networkId = normalizeNetworkId(network) +const createReadOnlyWalletClient = (chain: Chain, config: ReadOnlyWalletConfig): ReadOnlyWalletClient => { + const wallet = createReadOnlyWallet(config.address, config.rewardAddress) return { - // Direct Promise properties from wallet address: wallet.address, rewardAddress: wallet.rewardAddress, - // Metadata - networkId, - // Combinator methods + chain, attachProvider: (providerConfig) => { - return createReadOnlyClient(network, providerConfig, config) + return createReadOnlyClient(chain, providerConfig, config) }, - // Effect namespace - wallet's Effect interface Effect: wallet.Effect } } @@ -190,14 +110,12 @@ const createReadOnlyWalletClient = (network: NetworkId, config: ReadOnlyWalletCo * @category constructors */ const createReadOnlyClient = ( - network: NetworkId, + chain: Chain, providerConfig: ProviderConfig, - walletConfig: ReadOnlyWalletConfig, - slotConfig?: Time.SlotConfig + walletConfig: ReadOnlyWalletConfig ): ReadOnlyClient => { const provider = createProvider(providerConfig) - const walletNetwork = toWalletNetwork(network) - const wallet = createReadOnlyWallet(walletNetwork, walletConfig.address, walletConfig.rewardAddress) + const wallet = createReadOnlyWallet(walletConfig.address, walletConfig.rewardAddress) // Parse the bech32 address to Core Address for provider calls const coreAddress = CoreAddress.fromBech32(walletConfig.address) @@ -216,8 +134,7 @@ const createReadOnlyClient = ( return makeTxBuilder({ wallet, provider, - network: toBuilderNetwork(network), - slotConfig + slotConfig: chain.slotConfig }) }, Effect: { @@ -238,406 +155,68 @@ const createReadOnlyClient = ( } /** - * Extract all key hashes from a native script (recursively). - * This traverses ALL, ANY, and N-of-K scripts to find all ScriptPubKey key hashes. - * - * @since 2.0.0 - * @category utilities - */ -const extractKeyHashesFromNativeScript = (script: NativeScripts.NativeScriptVariants): Set => { - const keyHashes = new Set() - - const traverse = (s: NativeScripts.NativeScriptVariants): void => { - switch (s._tag) { - case "ScriptPubKey": - keyHashes.add(Bytes.toHex(s.keyHash)) - break - case "ScriptAll": - case "ScriptAny": - for (const nested of s.scripts) traverse(nested) - break - case "ScriptNOfK": - for (const nested of s.scripts) traverse(nested) - break - case "InvalidBefore": - case "InvalidHereafter": - // Time-based scripts don't contain key hashes - break - } - } - - traverse(script) - return keyHashes -} - -/** - * Determine key hashes that must sign a transaction based on inputs, withdrawals, certificates, - * and native scripts attached to the transaction or in reference inputs. - * - * @since 2.0.0 - * @category predicates - */ -const computeRequiredKeyHashesSync = (params: { - paymentKhHex?: string - rewardAddress?: CoreRewardAddress.RewardAddress | null - stakeKhHex?: string - tx: Transaction.Transaction - utxos: ReadonlyArray - referenceUtxos?: ReadonlyArray -}): Set => { - const required = new Set() - - if (params.tx.body.requiredSigners) { - for (const kh of params.tx.body.requiredSigners) required.add(KeyHash.toHex(kh)) - } - - // Extract key hashes from native scripts in the witness set - if (params.tx.witnessSet.nativeScripts) { - for (const nativeScript of params.tx.witnessSet.nativeScripts) { - const scriptKeyHashes = extractKeyHashesFromNativeScript(nativeScript.script) - for (const kh of scriptKeyHashes) required.add(kh) - } - } - - // Extract key hashes from native scripts in reference inputs - if (params.referenceUtxos) { - for (const utxo of params.referenceUtxos) { - if (utxo.scriptRef && utxo.scriptRef._tag === "NativeScript") { - const scriptKeyHashes = extractKeyHashesFromNativeScript(utxo.scriptRef.script) - for (const kh of scriptKeyHashes) required.add(kh) - } - } - } - - const ownedRefs = new Set(params.utxos.map((u) => CoreUTxO.toOutRefString(u))) - - const checkInputs = (inputs?: ReadonlyArray) => { - if (!inputs || !params.paymentKhHex) return - for (const input of inputs) { - const txIdHex = TransactionHash.toHex(input.transactionId) - const key = `${txIdHex}#${Number(input.index)}` - if (ownedRefs.has(key)) required.add(params.paymentKhHex) - } - } - checkInputs(params.tx.body.inputs) - if (params.tx.body.collateralInputs) checkInputs(params.tx.body.collateralInputs) - - if (params.tx.body.withdrawals && params.rewardAddress && params.stakeKhHex) { - const ourReward = Schema.decodeSync(CoreRewardAccount.FromBech32)(params.rewardAddress) - for (const [rewardAcc] of params.tx.body.withdrawals.withdrawals.entries()) { - if (Equal.equals(ourReward, rewardAcc)) { - required.add(params.stakeKhHex) - break - } - } - } - - if (params.tx.body.certificates && params.stakeKhHex) { - for (const cert of params.tx.body.certificates) { - const cred = - cert._tag === "StakeRegistration" || cert._tag === "StakeDeregistration" || cert._tag === "StakeDelegation" - ? cert.stakeCredential - : cert._tag === "RegCert" || cert._tag === "UnregCert" - ? cert.stakeCredential - : cert._tag === "StakeVoteDelegCert" || - cert._tag === "StakeRegDelegCert" || - cert._tag === "StakeVoteRegDelegCert" || - cert._tag === "VoteDelegCert" || - cert._tag === "VoteRegDelegCert" - ? cert.stakeCredential - : undefined - if (cred && cred._tag === "KeyHash") { - const khHex = KeyHash.toHex(cred) - if (khHex === params.stakeKhHex) required.add(params.stakeKhHex) - } - } - } - - return required -} - -/** - * Create signing wallet from seed phrase. + * Create a signing wallet from a seed phrase config. + * Delegates signing logic to Wallet.ts — no duplication here. * * @since 2.0.0 * @category constructors */ -const createSigningWallet = (network: WalletNew.Network, config: SeedWalletConfig): WalletNew.SigningWallet => { - const derivationEffect = Derivation.walletFromSeed(config.mnemonic, { - addressType: config.addressType ?? "Base", - accountIndex: config.accountIndex ?? 0, - password: config.password, - network - }).pipe(Effect.mapError((cause) => new WalletNew.WalletError({ message: cause.message, cause }))) - - // Effect implementations are the source of truth - const effectInterface: WalletNew.SigningWalletEffect = { - address: () => Effect.map(derivationEffect, (d) => d.address), - rewardAddress: () => Effect.map(derivationEffect, (d) => d.rewardAddress ?? null), - signTx: ( - txOrHex: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } - ) => - Effect.gen(function* () { - const derivation = yield* derivationEffect - - const tx = - typeof txOrHex === "string" - ? yield* ParseResult.decodeUnknownEither(Transaction.FromCBORHex())(txOrHex).pipe( - Effect.mapError( - (cause) => new WalletNew.WalletError({ message: `Failed to decode transaction: ${cause}`, cause }) - ) - ) - : txOrHex - const utxos = context?.utxos ?? [] - const referenceUtxos = context?.referenceUtxos ?? [] - - // Determine required key hashes for signing - const required = computeRequiredKeyHashesSync({ - paymentKhHex: derivation.paymentKhHex, - rewardAddress: derivation.rewardAddress ?? null, - stakeKhHex: derivation.stakeKhHex, - tx, - utxos, - referenceUtxos - }) - - // Build witnesses for keys we have - // When input is a hex string, hash the original CBOR bytes to preserve encoding. - // Re-encoding via hashTransaction(tx.body) can produce different bytes and a wrong hash. - const txHash = typeof txOrHex === "string" - ? hashTransactionRaw(Transaction.extractBodyBytes(Bytes.fromHex(txOrHex))) - : hashTransaction(tx.body) - const msg = txHash.hash - - const witnesses: Array = [] - const seenVKeys = new Set() - for (const khHex of required) { - const sk = derivation.keyStore.get(khHex) - if (!sk) continue - const sig = PrivateKey.sign(sk, msg) - const vk = VKey.fromPrivateKey(sk) - const vkHex = VKey.toHex(vk) - if (seenVKeys.has(vkHex)) continue - seenVKeys.add(vkHex) - witnesses.push(new TransactionWitnessSet.VKeyWitness({ vkey: vk, signature: sig })) - } - - return witnesses.length > 0 ? TransactionWitnessSet.fromVKeyWitnesses(witnesses) : TransactionWitnessSet.empty() - }), - signMessage: (_address: CoreAddress.Address | CoreRewardAddress.RewardAddress, payload: WalletNew.Payload) => - Effect.map(derivationEffect, (derivation) => { - // For now, always use payment key for message signing - const paymentSk = PrivateKey.fromBech32(derivation.paymentKey) - const vk = VKey.fromPrivateKey(paymentSk) - const bytes = typeof payload === "string" ? new TextEncoder().encode(payload) : payload - const _sig = PrivateKey.sign(paymentSk, bytes) - const sigHex = VKey.toHex(vk) // TODO: Convert signature properly - return { payload, signature: sigHex } - }) - } - - // Promise API runs the Effect implementations +const createSigningWallet = (chain: Chain, config: SeedWalletConfig): Wallet.SigningWallet => { + const effects = Wallet.makeSigningWalletEffect(chain.id, config.mnemonic, { + accountIndex: config.accountIndex, + addressType: config.addressType, + password: config.password + }) return { type: "signing", - address: () => Effect.runPromise(effectInterface.address()), - rewardAddress: () => Effect.runPromise(effectInterface.rewardAddress()), - signTx: (txOrHex, context) => Effect.runPromise(effectInterface.signTx(txOrHex, context)), - signMessage: (address, payload) => Effect.runPromise(effectInterface.signMessage(address, payload)), - Effect: effectInterface + address: () => runEffectPromise(effects.address()), + rewardAddress: () => runEffectPromise(effects.rewardAddress()), + signTx: (txOrHex, context) => runEffectPromise(effects.signTx(txOrHex, context)), + signMessage: (address, payload) => runEffectPromise(effects.signMessage(address, payload)), + Effect: effects } } /** - * Create a signing wallet from private keys. + * Create a signing wallet from a private key config. + * Delegates signing logic to Wallet.ts — no duplication here. * * @since 2.0.0 * @category constructors */ -const createPrivateKeyWallet = ( - network: WalletNew.Network, - config: PrivateKeyWalletConfig -): WalletNew.SigningWallet => { - const derivationEffect = Derivation.walletFromPrivateKey(config.paymentKey, { - stakeKeyBech32: config.stakeKey, - addressType: config.addressType ?? (config.stakeKey ? "Base" : "Enterprise"), - network - }).pipe(Effect.mapError((cause) => new WalletNew.WalletError({ message: cause.message, cause }))) - - const effectInterface: WalletNew.SigningWalletEffect = { - address: () => Effect.map(derivationEffect, (d) => d.address), - rewardAddress: () => Effect.map(derivationEffect, (d) => d.rewardAddress ?? null), - signTx: ( - txOrHex: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } - ) => - Effect.gen(function* () { - const derivation = yield* derivationEffect - - const tx = - typeof txOrHex === "string" - ? yield* ParseResult.decodeUnknownEither(Transaction.FromCBORHex())(txOrHex).pipe( - Effect.mapError( - (cause) => new WalletNew.WalletError({ message: `Failed to decode transaction: ${cause}`, cause }) - ) - ) - : txOrHex - const utxos = context?.utxos ?? [] - const referenceUtxos = context?.referenceUtxos ?? [] - - const required = computeRequiredKeyHashesSync({ - paymentKhHex: derivation.paymentKhHex, - rewardAddress: derivation.rewardAddress ?? null, - stakeKhHex: derivation.stakeKhHex, - tx, - utxos, - referenceUtxos - }) - - const txHash = typeof txOrHex === "string" - ? hashTransactionRaw(Transaction.extractBodyBytes(Bytes.fromHex(txOrHex))) - : hashTransaction(tx.body) - const msg = txHash.hash - - const witnesses: Array = [] - const seenVKeys = new Set() - for (const khHex of required) { - const sk = derivation.keyStore.get(khHex) - if (!sk) continue - const sig = PrivateKey.sign(sk, msg) - const vk = VKey.fromPrivateKey(sk) - const vkHex = VKey.toHex(vk) - if (seenVKeys.has(vkHex)) continue - seenVKeys.add(vkHex) - witnesses.push(new TransactionWitnessSet.VKeyWitness({ vkey: vk, signature: sig })) - } - - return witnesses.length > 0 ? TransactionWitnessSet.fromVKeyWitnesses(witnesses) : TransactionWitnessSet.empty() - }), - signMessage: (_address: CoreAddress.Address | CoreRewardAddress.RewardAddress, payload: WalletNew.Payload) => - Effect.map(derivationEffect, (derivation) => { - const paymentSk = PrivateKey.fromBech32(derivation.paymentKey) - const vk = VKey.fromPrivateKey(paymentSk) - const bytes = typeof payload === "string" ? new TextEncoder().encode(payload) : payload - const _sig = PrivateKey.sign(paymentSk, bytes) - const sigHex = VKey.toHex(vk) - return { payload, signature: sigHex } - }) - } - +const createPrivateKeyWallet = (chain: Chain, config: PrivateKeyWalletConfig): Wallet.SigningWallet => { + const effects = Wallet.makePrivateKeyWalletEffect(chain.id, config.paymentKey, { + stakeKey: config.stakeKey, + addressType: config.addressType + }) return { type: "signing", - address: () => runEffectPromise(effectInterface.address()), - rewardAddress: () => runEffectPromise(effectInterface.rewardAddress()), - signTx: (txOrHex, context) => runEffectPromise(effectInterface.signTx(txOrHex, context)), - signMessage: (address, payload) => runEffectPromise(effectInterface.signMessage(address, payload)), - Effect: effectInterface + address: () => runEffectPromise(effects.address()), + rewardAddress: () => runEffectPromise(effects.rewardAddress()), + signTx: (txOrHex, context) => runEffectPromise(effects.signTx(txOrHex, context)), + signMessage: (address, payload) => runEffectPromise(effects.signMessage(address, payload)), + Effect: effects } } /** - * Create an ApiWallet wrapping a CIP-30 browser wallet API. + * Create a CIP-30 API wallet. + * Delegates to Wallet.ts — no duplication here. * * @since 2.0.0 * @category constructors */ -const createApiWallet = (_network: WalletNew.Network, config: ApiWalletConfig): WalletNew.ApiWallet => { - const api = config.api - let cachedAddress: CoreAddress.Address | null = null - let cachedReward: CoreRewardAddress.RewardAddress | null = null - - const getPrimaryAddress = Effect.gen(function* () { - if (cachedAddress) return cachedAddress - const used = yield* Effect.tryPromise({ - try: () => api.getUsedAddresses(), - catch: (cause) => new WalletNew.WalletError({ message: (cause as Error).message, cause: cause as Error }) - }) - const unused = yield* Effect.tryPromise({ - try: () => api.getUnusedAddresses(), - catch: (cause) => new WalletNew.WalletError({ message: (cause as Error).message, cause: cause as Error }) - }) - const addrStr = used[0] ?? unused[0] - if (!addrStr) { - return yield* Effect.fail(new WalletNew.WalletError({ message: "Wallet API returned no addresses", cause: null })) - } - // Convert address string to Core Address - support both Bech32 and hex formats - try { - cachedAddress = CoreAddress.fromBech32(addrStr) - } catch { - // Fallback to hex if Bech32 fails (some wallets return hex) - try { - cachedAddress = CoreAddress.fromHex(addrStr) - } catch (error) { - return yield* Effect.fail( - new WalletNew.WalletError({ - message: `Invalid address format from wallet: ${addrStr}`, - cause: error as Error - }) - ) - } - } - return cachedAddress - }) - - const getPrimaryRewardAddress = Effect.gen(function* () { - if (cachedReward !== null) return cachedReward - const rewards = yield* Effect.tryPromise({ - try: () => api.getRewardAddresses(), - catch: (cause) => new WalletNew.WalletError({ message: (cause as Error).message, cause: cause as Error }) - }) - cachedReward = rewards[0] ? Schema.decodeSync(CoreRewardAddress.RewardAddress)(rewards[0]) : null - return cachedReward - }) - - // Effect implementations are the source of truth - const effectInterface: WalletNew.ApiWalletEffect = { - address: () => getPrimaryAddress, - rewardAddress: () => getPrimaryRewardAddress, - signTx: (txOrHex: Transaction.Transaction | string, _context?: { utxos?: ReadonlyArray }) => - Effect.gen(function* () { - const cbor = typeof txOrHex === "string" ? txOrHex : Transaction.toCBORHex(txOrHex) - const witnessHex = yield* Effect.tryPromise({ - try: () => api.signTx(cbor, true), - catch: (cause) => new WalletNew.WalletError({ message: "User rejected transaction signing", cause }) - }) - return yield* ParseResult.decodeUnknownEither(TransactionWitnessSet.FromCBORHex())(witnessHex).pipe( - Effect.mapError( - (cause) => new WalletNew.WalletError({ message: `Failed to decode witness set: ${cause}`, cause }) - ) - ) - }), - signMessage: (address: CoreAddress.Address | CoreRewardAddress.RewardAddress, payload: WalletNew.Payload) => - Effect.gen(function* () { - // Convert Core Address to bech32 string for the CIP-30 API - const addressStr = address instanceof CoreAddress.Address ? CoreAddress.toBech32(address) : address - const result = yield* Effect.tryPromise({ - try: () => api.signData(addressStr, payload), - catch: (cause) => new WalletNew.WalletError({ message: "User rejected message signing", cause }) - }) - return { payload, signature: result.signature } - }), - submitTx: (txOrHex: Transaction.Transaction | string) => - Effect.gen(function* () { - const cbor = typeof txOrHex === "string" ? txOrHex : Transaction.toCBORHex(txOrHex) - const txHashHex = yield* Effect.tryPromise({ - try: () => api.submitTx(cbor), - catch: (cause) => new WalletNew.WalletError({ message: (cause as Error).message, cause: cause as Error }) - }) - // Parse the string hash to TransactionHash - return Schema.decodeSync(TransactionHash.FromHex)(txHashHex) - }) - } - - // Promise API runs the Effect implementations +const createApiWallet = (config: ApiWalletConfig): Wallet.ApiWallet => { + const effects = Wallet.makeApiWalletEffect(config.api) return { - type: "api" as const, - api, - address: () => Effect.runPromise(effectInterface.address()), - rewardAddress: () => Effect.runPromise(effectInterface.rewardAddress()), - signTx: (txOrHex, context) => Effect.runPromise(effectInterface.signTx(txOrHex, context)), - signMessage: (address, payload) => Effect.runPromise(effectInterface.signMessage(address, payload)), - submitTx: (txOrHex) => Effect.runPromise(effectInterface.submitTx(txOrHex)), - Effect: effectInterface + type: "api", + api: config.api, + address: () => runEffectPromise(effects.address()), + rewardAddress: () => runEffectPromise(effects.rewardAddress()), + signTx: (txOrHex, context) => runEffectPromise(effects.signTx(txOrHex, context)), + signMessage: (address, payload) => runEffectPromise(effects.signMessage(address, payload)), + submitTx: (txOrHex) => runEffectPromise(effects.submitTx(txOrHex)), + Effect: effects } } @@ -650,19 +229,17 @@ const createApiWallet = (_network: WalletNew.Network, config: ApiWalletConfig): * @category constructors */ const createSigningWalletClient = ( - network: NetworkId, + chain: Chain, config: SeedWalletConfig | PrivateKeyWalletConfig ): SigningWalletClient => { - const walletNetwork = toWalletNetwork(network) const wallet = - config.type === "seed" ? createSigningWallet(walletNetwork, config) : createPrivateKeyWallet(walletNetwork, config) - const networkId = normalizeNetworkId(network) + config.type === "seed" ? createSigningWallet(chain, config) : createPrivateKeyWallet(chain, config) return { ...wallet, - networkId, + chain, attachProvider: (providerConfig) => { - return createSigningClient(network, providerConfig, config) + return createSigningClient(chain, providerConfig, config) } } } @@ -673,14 +250,13 @@ const createSigningWalletClient = ( * @since 2.0.0 * @category constructors */ -const createApiWalletClient = (network: NetworkId, config: ApiWalletConfig): ApiWalletClient => { - const walletNetwork = toWalletNetwork(network) - const wallet = createApiWallet(walletNetwork, config) +const createApiWalletClient = (chain: Chain, config: ApiWalletConfig): ApiWalletClient => { + const wallet = createApiWallet(config) return { ...wallet, attachProvider: (providerConfig) => { - return createSigningClient(network, providerConfig, config) + return createSigningClient(chain, providerConfig, config) } } } @@ -692,27 +268,25 @@ const createApiWalletClient = (network: NetworkId, config: ApiWalletConfig): Api * @category constructors */ const createSigningClient = ( - network: NetworkId, + chain: Chain, providerConfig: ProviderConfig, - walletConfig: SeedWalletConfig | PrivateKeyWalletConfig | ApiWalletConfig, - slotConfig?: Time.SlotConfig + walletConfig: SeedWalletConfig | PrivateKeyWalletConfig | ApiWalletConfig ): SigningClient => { const provider = createProvider(providerConfig) - const walletNetwork = toWalletNetwork(network) const wallet = walletConfig.type === "seed" - ? createSigningWallet(walletNetwork, walletConfig) + ? createSigningWallet(chain, walletConfig) : walletConfig.type === "private-key" - ? createPrivateKeyWallet(walletNetwork, walletConfig) - : createApiWallet(walletNetwork, walletConfig) + ? createPrivateKeyWallet(chain, walletConfig) + : createApiWallet(walletConfig) // Enhanced signTx that automatically fetches reference UTxOs from the network. // Passes the original txOrHex through to wallet.Effect.signTx to preserve CBOR bytes for hashing. const signTxWithAutoFetch = ( txOrHex: Transaction.Transaction | string, context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } - ): Effect.Effect => + ): Effect.Effect => Effect.gen(function* () { // If referenceUtxos already provided, pass original txOrHex through if (context?.referenceUtxos && context.referenceUtxos.length > 0) { @@ -724,7 +298,7 @@ const createSigningClient = ( typeof txOrHex === "string" ? yield* ParseResult.decodeUnknownEither(Transaction.FromCBORHex())(txOrHex).pipe( Effect.mapError( - (cause) => new WalletNew.WalletError({ message: `Failed to decode transaction: ${cause}`, cause }) + (cause) => new Wallet.WalletError({ message: `Failed to decode transaction: ${cause}`, cause }) ) ) : txOrHex @@ -734,7 +308,7 @@ const createSigningClient = ( if (tx.body.referenceInputs && tx.body.referenceInputs.length > 0) { referenceUtxos = yield* provider.Effect.getUtxosByOutRef(tx.body.referenceInputs).pipe( Effect.mapError( - (e) => new WalletNew.WalletError({ message: `Failed to fetch reference UTxOs: ${e.message}`, cause: e }) + (e) => new Wallet.WalletError({ message: `Failed to fetch reference UTxOs: ${e.message}`, cause: e }) ) ) } @@ -779,10 +353,9 @@ const createSigningClient = ( // The wallet is passed to the builder config, which handles address and UTxO resolution automatically // Protocol parameters are auto-fetched from provider during build() return makeTxBuilder({ - provider, // Pass provider for submission - wallet, // Pass wallet for signing - network: toBuilderNetwork(network), - slotConfig // Pass slot config for time conversion + provider, + wallet, + slotConfig: chain.slotConfig }) }, // Effect namespace @@ -790,26 +363,77 @@ const createSigningClient = ( } } +type ProviderAttachedClient = T extends SeedWalletConfig + ? SigningClient + : T extends PrivateKeyWalletConfig + ? SigningClient + : T extends ApiWalletConfig + ? SigningClient + : ReadOnlyClient + +type WalletOnlyAttachedClient = T extends SeedWalletConfig + ? SigningWalletClient + : T extends PrivateKeyWalletConfig + ? SigningWalletClient + : T extends ApiWalletConfig + ? ApiWalletClient + : ReadOnlyWalletClient + +/** + * Route a wallet config to the correct provider-backed legacy client constructor. + * + * @since 2.0.0 + * @category constructors + */ +const createProviderBackedClient = ( + chain: Chain, + providerConfig: ProviderConfig, + walletConfig: T +): ProviderAttachedClient => { + switch (walletConfig.type) { + case "read-only": + return createReadOnlyClient(chain, providerConfig, walletConfig) as ProviderAttachedClient + case "seed": + case "private-key": + case "api": + return createSigningClient(chain, providerConfig, walletConfig) as ProviderAttachedClient + } +} + +/** + * Route a wallet config to the correct wallet-only legacy client constructor. + * + * @since 2.0.0 + * @category constructors + */ +const createWalletOnlyClient = ( + chain: Chain, + walletConfig: T +): WalletOnlyAttachedClient => { + switch (walletConfig.type) { + case "read-only": + return createReadOnlyWalletClient(chain, walletConfig) as WalletOnlyAttachedClient + case "seed": + case "private-key": + return createSigningWalletClient(chain, walletConfig) as WalletOnlyAttachedClient + case "api": + return createApiWalletClient(chain, walletConfig) as WalletOnlyAttachedClient + } +} + /** * Create a ProviderOnlyClient by pairing a provider with network metadata and combinator method. * * @since 2.0.0 * @category constructors */ -const createProviderOnlyClient = (network: NetworkId, config: ProviderConfig): ProviderOnlyClient => { +const createProviderOnlyClient = (chain: Chain, config: ProviderConfig): ProviderOnlyClient => { const provider = createProvider(config) return { ...provider, attachWallet(walletConfig: T) { - switch (walletConfig.type) { - case "read-only": - return createReadOnlyClient(network, config, walletConfig) as any - case "seed": - return createSigningClient(network, config, walletConfig) as any - case "api": - return createSigningClient(network, config, walletConfig) as any - } + return createProviderBackedClient(chain, config, walletConfig) } } } @@ -820,41 +444,21 @@ const createProviderOnlyClient = (network: NetworkId, config: ProviderConfig): P * @since 2.0.0 * @category constructors */ -const createMinimalClient = (network: NetworkId = "mainnet"): MinimalClient => { - const networkId = normalizeNetworkId(network) - +const createMinimalClient = (chain: Chain): MinimalClient => { const effectInterface: MinimalClientEffect = { - networkId: Effect.succeed(networkId) + chain } return { - networkId, + chain, attachProvider: (config) => { - return createProviderOnlyClient(network, config) + return createProviderOnlyClient(chain, config) }, attachWallet(walletConfig: T) { - // TypeScript cannot narrow conditional return types from runtime discriminants. - // The conditional type interface provides type safety at call sites. - switch (walletConfig.type) { - case "read-only": - return createReadOnlyWalletClient(network, walletConfig) as any - case "seed": - return createSigningWalletClient(network, walletConfig) as any - case "api": - return createApiWalletClient(network, walletConfig) as any - } + return createWalletOnlyClient(chain, walletConfig) }, attach(providerConfig: ProviderConfig, walletConfig: TW) { - // TypeScript cannot narrow conditional return types from runtime discriminants. - // The conditional type interface provides type safety at call sites. - switch (walletConfig.type) { - case "read-only": - return createReadOnlyClient(network, providerConfig, walletConfig) as any - case "seed": - return createSigningClient(network, providerConfig, walletConfig) as any - case "api": - return createSigningClient(network, providerConfig, walletConfig) as any - } + return createProviderBackedClient(chain, providerConfig, walletConfig) }, // Effect namespace Effect: effectInterface @@ -868,6 +472,9 @@ const createMinimalClient = (network: NetworkId = "mainnet"): MinimalClient => { * provider and wallet → full-featured client; provider only → query and submission; * wallet only → signing with network metadata; network only → minimal context with combinators. * + * @deprecated Use the composable `client(chain).with(provider).with(wallet)` API instead. + * See `Client.ts` for the new pattern. + * * @since 2.0.0 * @category constructors */ @@ -875,60 +482,55 @@ const createMinimalClient = (network: NetworkId = "mainnet"): MinimalClient => { // Most specific overloads first - wallet type determines client capability // Provider + ReadOnly Wallet → ReadOnlyClient export function createClient(config: { - network?: NetworkId + chain: Chain provider: ProviderConfig wallet: ReadOnlyWalletConfig - slotConfig?: Time.SlotConfig }): ReadOnlyClient // Provider + Seed Wallet → SigningClient export function createClient(config: { - network?: NetworkId + chain: Chain provider: ProviderConfig wallet: SeedWalletConfig - slotConfig?: Time.SlotConfig }): SigningClient // Provider + PrivateKey Wallet → SigningClient export function createClient(config: { - network?: NetworkId + chain: Chain provider: ProviderConfig wallet: PrivateKeyWalletConfig - slotConfig?: Time.SlotConfig }): SigningClient // Provider + API Wallet → SigningClient export function createClient(config: { - network?: NetworkId + chain: Chain provider: ProviderConfig wallet: ApiWalletConfig - slotConfig?: Time.SlotConfig }): SigningClient // Provider only → ProviderOnlyClient -export function createClient(config: { network?: NetworkId; provider: ProviderConfig }): ProviderOnlyClient +export function createClient(config: { chain: Chain; provider: ProviderConfig }): ProviderOnlyClient // ReadOnly Wallet only → ReadOnlyWalletClient -export function createClient(config: { network?: NetworkId; wallet: ReadOnlyWalletConfig }): ReadOnlyWalletClient +export function createClient(config: { chain: Chain; wallet: ReadOnlyWalletConfig }): ReadOnlyWalletClient // Seed Wallet only → SigningWalletClient -export function createClient(config: { network?: NetworkId; wallet: SeedWalletConfig }): SigningWalletClient +export function createClient(config: { chain: Chain; wallet: SeedWalletConfig }): SigningWalletClient // Private Key Wallet only → SigningWalletClient -export function createClient(config: { network?: NetworkId; wallet: PrivateKeyWalletConfig }): SigningWalletClient +export function createClient(config: { chain: Chain; wallet: PrivateKeyWalletConfig }): SigningWalletClient // API Wallet only → ApiWalletClient -export function createClient(config: { network?: NetworkId; wallet: ApiWalletConfig }): ApiWalletClient +export function createClient(config: { chain: Chain; wallet: ApiWalletConfig }): ApiWalletClient -// Network only or minimal → MinimalClient -export function createClient(config?: { network?: NetworkId }): MinimalClient +// Chain only → MinimalClient +export function createClient(config: { chain: Chain }): MinimalClient // Implementation signature - handles all cases (all synchronous now) -export function createClient(config?: { - network?: NetworkId +export function createClient(config: { + chain: Chain provider?: ProviderConfig wallet?: WalletConfig - slotConfig?: Time.SlotConfig }): | MinimalClient | ReadOnlyClient @@ -937,38 +539,19 @@ export function createClient(config?: { | ReadOnlyWalletClient | SigningWalletClient | ApiWalletClient { - const network = config?.network ?? "mainnet" - const slotConfig = config?.slotConfig - - if (config?.provider && config?.wallet) { - switch (config.wallet.type) { - case "read-only": - return createReadOnlyClient(network, config.provider, config.wallet, slotConfig) - case "seed": - return createSigningClient(network, config.provider, config.wallet, slotConfig) - case "private-key": - return createSigningClient(network, config.provider, config.wallet, slotConfig) - case "api": - return createSigningClient(network, config.provider, config.wallet, slotConfig) - } + const chain = config.chain + + if (config.provider && config.wallet) { + return createProviderBackedClient(chain, config.provider, config.wallet) } - if (config?.wallet) { - switch (config.wallet.type) { - case "read-only": - return createReadOnlyWalletClient(network, config.wallet) - case "seed": - return createSigningWalletClient(network, config.wallet) - case "private-key": - return createSigningWalletClient(network, config.wallet) - case "api": - return createApiWalletClient(network, config.wallet) - } + if (config.wallet) { + return createWalletOnlyClient(chain, config.wallet) } - if (config?.provider) { - return createProviderOnlyClient(network, config.provider) + if (config.provider) { + return createProviderOnlyClient(chain, config.provider) } - return createMinimalClient(network) + return createMinimalClient(chain) } diff --git a/packages/evolution/src/sdk/client/ClientLegacy.ts b/packages/evolution/src/sdk/client/ClientLegacy.ts new file mode 100644 index 00000000..7ec7f2e6 --- /dev/null +++ b/packages/evolution/src/sdk/client/ClientLegacy.ts @@ -0,0 +1,328 @@ +import { Data, type Effect, type Schedule } from "effect" + +import type * as CoreUTxO from "../../UTxO.js" +import type { ReadOnlyTransactionBuilder, SigningTransactionBuilder } from "../builders/TransactionBuilder.js" +import type * as Provider from "../provider/Provider.js" +import type { EffectToPromiseAPI } from "../Type.js" +import type { + ApiWalletEffect, + ReadOnlyWalletEffect, + SigningWalletEffect, + WalletApi, + WalletError +} from "../wallet/Wallet.js" +import type { Chain } from "./Chain.js" + +/** + * Error class for provider-related operations. + * + * @since 2.0.0 + * @category errors + */ +export class ProviderError extends Data.TaggedError("ProviderError")<{ + message?: string + cause?: unknown +}> {} + +/** + * MinimalClient Effect - holds chain context. + * + * @since 2.0.0 + * @category model + */ +export interface MinimalClientEffect { + readonly chain: Chain +} + +/** + * ReadOnlyClient Effect - provider, read-only wallet, and utility methods. + * + * @since 2.0.0 + * @category model + */ +export interface ReadOnlyClientEffect extends Provider.ProviderEffect, ReadOnlyWalletEffect { + readonly getWalletUtxos: () => Effect.Effect, Provider.ProviderError> + readonly getWalletDelegation: () => Effect.Effect +} + +/** + * SigningClient Effect - provider, signing wallet, and utility methods. + * + * @since 2.0.0 + * @category model + */ +export interface SigningClientEffect extends Provider.ProviderEffect, SigningWalletEffect { + readonly getWalletUtxos: () => Effect.Effect, WalletError | Provider.ProviderError> + readonly getWalletDelegation: () => Effect.Effect +} + +/** + * MinimalClient - network context with combinator methods to attach provider and/or wallet. + * + * @since 2.0.0 + * @category model + */ +export interface MinimalClient { + readonly chain: Chain + readonly attachProvider: (config: ProviderConfig) => ProviderOnlyClient + readonly attachWallet: ( + config: T + ) => T extends SeedWalletConfig + ? SigningWalletClient + : T extends PrivateKeyWalletConfig + ? SigningWalletClient + : T extends ApiWalletConfig + ? ApiWalletClient + : ReadOnlyWalletClient + readonly attach: ( + providerConfig: ProviderConfig, + walletConfig: TW + ) => TW extends SeedWalletConfig + ? SigningClient + : TW extends PrivateKeyWalletConfig + ? SigningClient + : TW extends ApiWalletConfig + ? SigningClient + : ReadOnlyClient + readonly Effect: MinimalClientEffect +} + +/** + * ProviderOnlyClient - blockchain queries and transaction submission. + * + * @since 2.0.0 + * @category model + */ +export type ProviderOnlyClient = EffectToPromiseAPI & { + readonly attachWallet: ( + config: T + ) => T extends SeedWalletConfig + ? SigningClient + : T extends PrivateKeyWalletConfig + ? SigningClient + : T extends ApiWalletConfig + ? SigningClient + : ReadOnlyClient + readonly Effect: Provider.ProviderEffect +} + +/** + * ReadOnlyClient - blockchain queries and wallet address operations without signing. + * Use newTx() to build unsigned transactions. + * + * @since 2.0.0 + * @category model + */ +export type ReadOnlyClient = EffectToPromiseAPI & { + readonly newTx: (utxos?: ReadonlyArray) => ReadOnlyTransactionBuilder + readonly Effect: ReadOnlyClientEffect +} + +/** + * SigningClient - full functionality: blockchain queries, transaction signing, and submission. + * Use newTx() to build, sign, and submit transactions. + * + * @since 2.0.0 + * @category model + */ +export type SigningClient = EffectToPromiseAPI & { + readonly newTx: () => SigningTransactionBuilder + readonly Effect: SigningClientEffect +} + +/** + * ApiWalletClient - CIP-30 wallet signing and submission without blockchain queries. + * Requires attachProvider() to access blockchain data. + * + * @since 2.0.0 + * @category model + */ +export type ApiWalletClient = EffectToPromiseAPI & { + readonly attachProvider: (config: ProviderConfig) => SigningClient + readonly Effect: ApiWalletEffect +} + +/** + * SigningWalletClient - transaction signing without blockchain queries. + * Requires attachProvider() to access blockchain data. + * + * @since 2.0.0 + * @category model + */ +export type SigningWalletClient = EffectToPromiseAPI & { + readonly chain: Chain + readonly attachProvider: (config: ProviderConfig) => SigningClient + readonly Effect: SigningWalletEffect +} + +/** + * ReadOnlyWalletClient - wallet address access without signing or blockchain queries. + * Requires attachProvider() to access blockchain data. + * + * @since 2.0.0 + * @category model + */ +export type ReadOnlyWalletClient = EffectToPromiseAPI & { + readonly chain: Chain + readonly attachProvider: (config: ProviderConfig) => ReadOnlyClient + readonly Effect: ReadOnlyWalletEffect +} + +/** + * Retry policy configuration with exponential backoff. + * + * @since 2.0.0 + * @category model + */ +export interface RetryConfig { + readonly maxRetries: number + readonly retryDelayMs: number + readonly backoffMultiplier: number + readonly maxRetryDelayMs: number +} + +/** + * Preset retry configurations for common scenarios. + * + * @since 2.0.0 + * @category constants + */ +export const RetryPresets = { + none: { maxRetries: 0, retryDelayMs: 0, backoffMultiplier: 1, maxRetryDelayMs: 0 } as const, + fast: { maxRetries: 3, retryDelayMs: 500, backoffMultiplier: 1.5, maxRetryDelayMs: 5000 } as const, + standard: { maxRetries: 3, retryDelayMs: 1000, backoffMultiplier: 2, maxRetryDelayMs: 10000 } as const, + aggressive: { maxRetries: 5, retryDelayMs: 1000, backoffMultiplier: 2, maxRetryDelayMs: 30000 } as const +} as const + +/** + * Retry policy - preset config, custom schedule, or preset reference. + * + * @since 2.0.0 + * @category model + */ +export type RetryPolicy = RetryConfig | Schedule.Schedule | { preset: keyof typeof RetryPresets } + +/** + * Blockfrost provider configuration. + * + * @since 2.0.0 + * @category model + */ +export interface BlockfrostConfig { + readonly type: "blockfrost" + readonly baseUrl: string + readonly projectId?: string + readonly retryPolicy?: RetryPolicy +} + +/** + * Kupmios provider configuration (Kupo + Ogmios). + * + * @since 2.0.0 + * @category model + */ +export interface KupmiosConfig { + readonly type: "kupmios" + readonly kupoUrl: string + readonly ogmiosUrl: string + readonly headers?: { + readonly ogmiosHeader?: Record + readonly kupoHeader?: Record + } + readonly retryPolicy?: RetryPolicy +} + +/** + * Maestro provider configuration. + * + * @since 2.0.0 + * @category model + */ +export interface MaestroConfig { + readonly type: "maestro" + readonly baseUrl: string + readonly apiKey: string + readonly turboSubmit?: boolean + readonly retryPolicy?: RetryPolicy +} + +/** + * Koios provider configuration. + * + * @since 2.0.0 + * @category model + */ +export interface KoiosConfig { + readonly type: "koios" + readonly baseUrl: string + readonly token?: string + readonly retryPolicy?: RetryPolicy +} + +/** + * Provider configuration union type. + * + * @since 2.0.0 + * @category model + */ +export type ProviderConfig = BlockfrostConfig | KupmiosConfig | MaestroConfig | KoiosConfig + +/** + * Seed phrase wallet configuration. + * + * @since 2.0.0 + * @category model + */ +export interface SeedWalletConfig { + readonly type: "seed" + readonly mnemonic: string + readonly accountIndex?: number + readonly paymentIndex?: number + readonly stakeIndex?: number + readonly addressType?: "Base" | "Enterprise" + readonly password?: string +} + +/** + * Private key wallet configuration. + * + * @since 2.0.0 + * @category model + */ +export interface PrivateKeyWalletConfig { + readonly type: "private-key" + readonly paymentKey: string + readonly stakeKey?: string + readonly addressType?: "Base" | "Enterprise" +} + +/** + * Read-only wallet configuration. + * + * @since 2.0.0 + * @category model + */ +export interface ReadOnlyWalletConfig { + readonly type: "read-only" + readonly address: string + readonly rewardAddress?: string +} + +/** + * CIP-30 API wallet configuration. + * + * @since 2.0.0 + * @category model + */ +export interface ApiWalletConfig { + readonly type: "api" + readonly api: WalletApi +} + +/** + * Wallet configuration union type. + * + * @since 2.0.0 + * @category model + */ +export type WalletConfig = SeedWalletConfig | PrivateKeyWalletConfig | ReadOnlyWalletConfig | ApiWalletConfig diff --git a/packages/evolution/src/sdk/client/Koios.ts b/packages/evolution/src/sdk/client/Koios.ts new file mode 100644 index 00000000..9ece40c5 --- /dev/null +++ b/packages/evolution/src/sdk/client/Koios.ts @@ -0,0 +1,70 @@ +/** + * Koios provider for the composable client API. + * + * Adds query, submission, and await capabilities. + * + * @example + * ```ts + * import { client, mainnet, koios } from "@evolution-sdk/evolution" + * + * const myClient = client(mainnet) + * .with(koios({ baseUrl: "https://api.koios.rest/api/v1" })) + * ``` + * + * @since 2.1.0 + * @module + */ + +import * as KoiosEffect from "../provider/internal/KoiosEffect.js" +import { attachCapabilities } from "./attachCapabilities.js" +import type { KoiosCapabilities } from "./Capabilities.js" +import { type Client } from "./Client.js" + +// ── Configuration ───────────────────────────────────────────────────────────── + +/** + * Configuration for the Koios provider. + * + * @since 2.1.0 + * @category model + */ +export interface KoiosConfig { + readonly baseUrl: string + readonly token?: string +} + +// ── Constructor ─────────────────────────────────────────────────────────────── + +/** + * Koios provider constructor. + * + * Adds query, submission, and await capabilities. + * + * @example + * ```ts + * import { client, mainnet, koios } from "@evolution-sdk/evolution" + * + * const myClient = client(mainnet) + * .with(koios({ baseUrl: "https://api.koios.rest/api/v1" })) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const koios = (cfg: KoiosConfig) => + ( + c: T + ): T & KoiosCapabilities => { + return attachCapabilities(c, { + getUtxos: KoiosEffect.getUtxos(cfg.baseUrl, cfg.token), + getUtxosByOutRef: KoiosEffect.getUtxosByOutRef(cfg.baseUrl, cfg.token), + getUtxosWithUnit: KoiosEffect.getUtxosWithUnit(cfg.baseUrl, cfg.token), + getUtxoByUnit: KoiosEffect.getUtxoByUnit(cfg.baseUrl, cfg.token), + getProtocolParameters: () => KoiosEffect.getProtocolParameters(cfg.baseUrl, cfg.token), + getDelegation: KoiosEffect.getDelegation(cfg.baseUrl, cfg.token), + submitTx: KoiosEffect.submitTx(cfg.baseUrl, cfg.token), + getDatum: KoiosEffect.getDatum(cfg.baseUrl, cfg.token), + awaitTx: KoiosEffect.awaitTx(cfg.baseUrl, cfg.token), + evaluateTx: KoiosEffect.evaluateTx(cfg.baseUrl, cfg.token) + }) + } diff --git a/packages/evolution/src/sdk/client/Kupmios.ts b/packages/evolution/src/sdk/client/Kupmios.ts new file mode 100644 index 00000000..4418fa98 --- /dev/null +++ b/packages/evolution/src/sdk/client/Kupmios.ts @@ -0,0 +1,75 @@ +/** + * Kupmios provider (Kupo + Ogmios) for the composable client API. + * + * Adds query, submission, evaluation, and await capabilities. + * + * @example + * ```ts + * import { client, preview, kupmios } from "@evolution-sdk/evolution" + * + * const myClient = client(preview) + * .with(kupmios({ kupoUrl: "http://localhost:1442", ogmiosUrl: "ws://localhost:1337" })) + * ``` + * + * @since 2.1.0 + * @module + */ + +import * as KupmiosEffects from "../provider/internal/KupmiosEffects.js" +import { attachCapabilities } from "./attachCapabilities.js" +import type { KupmiosCapabilities } from "./Capabilities.js" +import { type Client } from "./Client.js" + +// ── Configuration ───────────────────────────────────────────────────────────── + +/** + * Configuration for the Kupmios provider (Kupo + Ogmios). + * + * @since 2.1.0 + * @category model + */ +export interface KupmiosConfig { + readonly kupoUrl: string + readonly ogmiosUrl: string + readonly headers?: { + readonly ogmiosHeader?: Record + readonly kupoHeader?: Record + } +} + +// ── Constructor ─────────────────────────────────────────────────────────────── + +/** + * Kupmios provider constructor (Kupo + Ogmios combined). + * + * Adds query, submission, evaluation, and await capabilities. + * + * @example + * ```ts + * import { client, preview, kupmios } from "@evolution-sdk/evolution" + * + * const myClient = client(preview) + * .with(kupmios({ kupoUrl: "http://localhost:1442", ogmiosUrl: "ws://localhost:1337" })) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const kupmios = (cfg: KupmiosConfig) => + ( + c: T + ): T & KupmiosCapabilities => { + return attachCapabilities(c, { + getUtxos: KupmiosEffects.getUtxosEffect(cfg.kupoUrl, cfg.headers), + getUtxosByOutRef: KupmiosEffects.getUtxosByOutRefEffect(cfg.kupoUrl, cfg.headers), + getUtxosWithUnit: KupmiosEffects.getUtxosWithUnitEffect(cfg.kupoUrl, cfg.headers), + getUtxoByUnit: KupmiosEffects.getUtxoByUnitEffect(cfg.kupoUrl, cfg.headers), + getProtocolParameters: () => KupmiosEffects.getProtocolParametersEffect(cfg.ogmiosUrl, cfg.headers), + getDelegation: KupmiosEffects.getDelegationEffect(cfg.ogmiosUrl, cfg.headers), + submitTx: KupmiosEffects.submitTxEffect(cfg.ogmiosUrl, cfg.headers), + evaluateTx: KupmiosEffects.evaluateTxEffect(cfg.ogmiosUrl, cfg.headers), + awaitTx: KupmiosEffects.awaitTxEffect(cfg.kupoUrl, cfg.headers), + getDatum: KupmiosEffects.getDatumEffect(cfg.kupoUrl, cfg.headers), + watchUtxos: KupmiosEffects.watchUtxosEffect(cfg.kupoUrl, cfg.headers) + }) + } diff --git a/packages/evolution/src/sdk/client/Maestro.ts b/packages/evolution/src/sdk/client/Maestro.ts new file mode 100644 index 00000000..eb4bb9b4 --- /dev/null +++ b/packages/evolution/src/sdk/client/Maestro.ts @@ -0,0 +1,71 @@ +/** + * Maestro provider for the composable client API. + * + * Adds query, submission, evaluation, and await capabilities. + * + * @example + * ```ts + * import { client, mainnet, maestro } from "@evolution-sdk/evolution" + * + * const myClient = client(mainnet) + * .with(maestro({ baseUrl: "https://mainnet.gomaestro-api.org/v1", apiKey: "..." })) + * ``` + * + * @since 2.1.0 + * @module + */ + +import * as MaestroEffect from "../provider/internal/MaestroEffect.js" +import { attachCapabilities } from "./attachCapabilities.js" +import type { MaestroCapabilities } from "./Capabilities.js" +import { type Client } from "./Client.js" + +// ── Configuration ───────────────────────────────────────────────────────────── + +/** + * Configuration for the Maestro provider. + * + * @since 2.1.0 + * @category model + */ +export interface MaestroConfig { + readonly baseUrl: string + readonly apiKey: string + readonly turboSubmit?: boolean +} + +// ── Constructor ─────────────────────────────────────────────────────────────── + +/** + * Maestro provider constructor. + * + * Adds query, submission, evaluation, and await capabilities. + * + * @example + * ```ts + * import { client, mainnet, maestro } from "@evolution-sdk/evolution" + * + * const myClient = client(mainnet) + * .with(maestro({ baseUrl: "https://mainnet.gomaestro-api.org/v1", apiKey: "..." })) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const maestro = (cfg: MaestroConfig) => + ( + c: T + ): T & MaestroCapabilities => { + return attachCapabilities(c, { + getUtxos: MaestroEffect.getUtxos(cfg.baseUrl, cfg.apiKey), + getUtxosByOutRef: MaestroEffect.getUtxosByOutRef(cfg.baseUrl, cfg.apiKey), + getUtxosWithUnit: MaestroEffect.getUtxosWithUnit(cfg.baseUrl, cfg.apiKey), + getUtxoByUnit: MaestroEffect.getUtxoByUnit(cfg.baseUrl, cfg.apiKey), + getProtocolParameters: () => MaestroEffect.getProtocolParameters(cfg.baseUrl, cfg.apiKey), + getDelegation: MaestroEffect.getDelegation(cfg.baseUrl, cfg.apiKey), + submitTx: MaestroEffect.submitTx(cfg.baseUrl, cfg.apiKey, cfg.turboSubmit), + evaluateTx: MaestroEffect.evaluateTx(cfg.baseUrl, cfg.apiKey), + awaitTx: MaestroEffect.awaitTx(cfg.baseUrl, cfg.apiKey), + getDatum: MaestroEffect.getDatum(cfg.baseUrl, cfg.apiKey) + }) + } diff --git a/packages/evolution/src/sdk/client/Wallets.ts b/packages/evolution/src/sdk/client/Wallets.ts new file mode 100644 index 00000000..bcaf54e7 --- /dev/null +++ b/packages/evolution/src/sdk/client/Wallets.ts @@ -0,0 +1,302 @@ +/** + * Wallet constructors for the composable client API. + * + * Provides wallet constructors that add wallet capabilities to a base + * `client(chain)`. Each constructor returns a function that intersects + * wallet capabilities onto the client type. + * + * @example + * ```ts + * import { client, preview, blockfrost, seedWallet } from "@evolution-sdk/evolution" + * + * const myClient = client(preview) + * .with(blockfrost({ baseUrl: "...", projectId: "..." })) + * .with(seedWallet({ mnemonic: "..." })) + * ``` + * + * @since 2.1.0 + * @module + */ + +import { Effect, ParseResult, Schema } from "effect" + +import * as Address from "../../Address.js" +import * as CoreRewardAddress from "../../RewardAddress.js" +import * as Transaction from "../../Transaction.js" +import type * as TransactionWitnessSet from "../../TransactionWitnessSet.js" +import type * as CoreUTxO from "../../UTxO.js" +import type { ProviderError } from "../provider/Provider.js" +import type { WalletApi, WalletError } from "../wallet/Wallet.js" +import * as Wallet from "../wallet/Wallet.js" +import { attachCapabilities } from "./attachCapabilities.js" +import type { + Addressable, + Cip30WalletCapabilities, + QueryDelegation, + QueryUtxos, + QueryUtxosByOutRef, + SigningWalletCapabilities, + Stakeable, + WalletDelegation, + WalletUtxos +} from "./Capabilities.js" +import { type Client } from "./Client.js" + +// ── Wallet configs ──────────────────────────────────────────────────────────── + +/** + * Configuration for the seed wallet constructor. + * + * @since 2.1.0 + * @category model + */ +export interface SeedWalletConfig { + readonly mnemonic: string + readonly accountIndex?: number + readonly paymentIndex?: number + readonly stakeIndex?: number + readonly addressType?: "Base" | "Enterprise" + readonly password?: string +} + +/** + * Configuration for the private key wallet constructor. + * + * @since 2.1.0 + * @category model + */ +export interface PrivateKeyWalletConfig { + readonly paymentKey: string + readonly stakeKey?: string + readonly addressType?: "Base" | "Enterprise" +} + +// ── Wallet capability type aliases ──────────────────────────────────────────── + +type SigningCaps = SigningWalletCapabilities & + (T extends { Effect: { getUtxos: unknown } } ? WalletUtxos : {}) & + (T extends { Effect: { getDelegation: unknown } } ? WalletDelegation : {}) + +type ReadOnlyWalletCaps = Addressable & Stakeable + +type Cip30WalletCaps = Cip30WalletCapabilities + +// ── Wallet constructors ─────────────────────────────────────────────────────── + +/** + * Seed phrase wallet constructor. + * + * Adds address, signing, staking, and (when a provider is present) wallet UTxO capabilities. + * + * @example + * ```ts + * import { client, preview, blockfrost, seedWallet } from "@evolution-sdk/evolution" + * + * const myClient = client(preview) + * .with(blockfrost({ baseUrl: "...", projectId: "..." })) + * .with(seedWallet({ mnemonic: "your 24 word mnemonic ..." })) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const seedWallet = (cfg: SeedWalletConfig) => + (c: T): T & SigningCaps => { + const effects = Wallet.makeSigningWalletEffect(c.chain.id, cfg.mnemonic, { + accountIndex: cfg.accountIndex, + paymentIndex: cfg.paymentIndex, + stakeIndex: cfg.stakeIndex, + addressType: cfg.addressType, + password: cfg.password + }) + + const providerEffect = c.Effect as Partial + const caps: Record) => Effect.Effect> = { + getAddress: effects.address, + getRewardAddress: effects.rewardAddress, + signMessage: effects.signMessage as never + } + + // Auto-fetch reference UTxOs before signing when a provider is present + if (typeof providerEffect.getUtxosByOutRef === "function") { + const getUtxosByOutRef = providerEffect.getUtxosByOutRef + caps.signTx = (( + txOrHex: Transaction.Transaction | string, + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } + ) => + Effect.gen(function* () { + if (!context?.referenceUtxos?.length) { + const tx = + typeof txOrHex === "string" + ? yield* ParseResult.decodeUnknownEither(Transaction.FromCBORHex())(txOrHex).pipe( + Effect.mapError( + (cause) => new Wallet.WalletError({ message: `Failed to decode transaction: ${cause}`, cause }) + ) + ) + : txOrHex + if (tx.body.referenceInputs && tx.body.referenceInputs.length > 0) { + const fetched = yield* (getUtxosByOutRef(tx.body.referenceInputs) as Effect.Effect, ProviderError>).pipe( + Effect.orElseSucceed(() => [] as ReadonlyArray) + ) + return yield* (effects.signTx(txOrHex, { ...context, referenceUtxos: fetched }) as Effect.Effect) + } + } + return yield* (effects.signTx(txOrHex, context) as Effect.Effect) + })) as never + } else { + caps.signTx = effects.signTx as never + } + + if (typeof providerEffect.getUtxos === "function") { + const getUtxos = providerEffect.getUtxos + caps.getWalletUtxos = () => Effect.flatMap(effects.address(), (addr) => getUtxos(addr)) + } + if (typeof providerEffect.getDelegation === "function") { + const getDelegation = providerEffect.getDelegation + caps.getWalletDelegation = () => + Effect.flatMap(effects.rewardAddress(), (rewardAddr) => { + if (!rewardAddr) return Effect.fail(new Wallet.WalletError({ message: "No reward address", cause: null })) + return getDelegation(rewardAddr) as Effect.Effect + }) + } + + return attachCapabilities>(c, caps) + } + +/** + * Private key wallet constructor. + * + * Adds address, signing, staking, and (when a provider is present) wallet UTxO capabilities. + * + * @example + * ```ts + * import { client, preview, blockfrost, privateKeyWallet } from "@evolution-sdk/evolution" + * + * const myClient = client(preview) + * .with(blockfrost({ baseUrl: "...", projectId: "..." })) + * .with(privateKeyWallet({ paymentKey: "ed25519e_sk..." })) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const privateKeyWallet = (cfg: PrivateKeyWalletConfig) => + (c: T): T & SigningCaps => { + const effects = Wallet.makePrivateKeyWalletEffect(c.chain.id, cfg.paymentKey, { + stakeKey: cfg.stakeKey, + addressType: cfg.addressType + }) + + const providerEffect = c.Effect as Partial + const caps: Record) => Effect.Effect> = { + getAddress: effects.address, + getRewardAddress: effects.rewardAddress, + signMessage: effects.signMessage as never + } + + // Auto-fetch reference UTxOs before signing when a provider is present + if (typeof providerEffect.getUtxosByOutRef === "function") { + const getUtxosByOutRef = providerEffect.getUtxosByOutRef + caps.signTx = (( + txOrHex: Transaction.Transaction | string, + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } + ) => + Effect.gen(function* () { + if (!context?.referenceUtxos?.length) { + const tx = + typeof txOrHex === "string" + ? yield* ParseResult.decodeUnknownEither(Transaction.FromCBORHex())(txOrHex).pipe( + Effect.mapError( + (cause) => new Wallet.WalletError({ message: `Failed to decode transaction: ${cause}`, cause }) + ) + ) + : txOrHex + if (tx.body.referenceInputs && tx.body.referenceInputs.length > 0) { + const fetched = yield* (getUtxosByOutRef(tx.body.referenceInputs) as Effect.Effect, ProviderError>).pipe( + Effect.orElseSucceed(() => [] as ReadonlyArray) + ) + return yield* (effects.signTx(txOrHex, { ...context, referenceUtxos: fetched }) as Effect.Effect) + } + } + return yield* (effects.signTx(txOrHex, context) as Effect.Effect) + })) as never + } else { + caps.signTx = effects.signTx as never + } + + if (typeof providerEffect.getUtxos === "function") { + const getUtxos = providerEffect.getUtxos + caps.getWalletUtxos = () => Effect.flatMap(effects.address(), (addr) => getUtxos(addr)) + } + if (typeof providerEffect.getDelegation === "function") { + const getDelegation = providerEffect.getDelegation + caps.getWalletDelegation = () => + Effect.flatMap(effects.rewardAddress(), (rewardAddr) => { + if (!rewardAddr) return Effect.fail(new Wallet.WalletError({ message: "No reward address", cause: null })) + return getDelegation(rewardAddr) as Effect.Effect + }) + } + + return attachCapabilities>(c, caps) + } + +/** + * Read-only wallet constructor. + * + * Adds address and reward address capabilities — no signing. + * + * @example + * ```ts + * import { client, mainnet, blockfrost, readOnlyWallet } from "@evolution-sdk/evolution" + * + * const myClient = client(mainnet) + * .with(blockfrost({ baseUrl: "...", projectId: "..." })) + * .with(readOnlyWallet("addr1...")) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const readOnlyWallet = (address: string, rewardAddress?: string) => + (c: T): T & ReadOnlyWalletCaps => { + const parsed = Address.fromBech32(address) + const parsedReward = rewardAddress + ? Schema.decodeSync(CoreRewardAddress.RewardAddress)(rewardAddress) + : null + const effects = Wallet.makeReadOnlyWalletEffect(parsed, parsedReward) + + return attachCapabilities(c, { + getAddress: effects.address, + getRewardAddress: effects.rewardAddress + }) + } + +/** + * CIP-30 browser wallet constructor. + * + * Adds address, signing, staking, and wallet-based submission capabilities. + * + * @example + * ```ts + * import { client, mainnet, cip30Wallet } from "@evolution-sdk/evolution" + * + * const api = await window.cardano.nami.enable() + * const myClient = client(mainnet) + * .with(cip30Wallet(api)) + * ``` + * + * @since 2.1.0 + * @category constructors + */ +export const cip30Wallet = (api: WalletApi) => + (c: T): T & Cip30WalletCaps => { + const effects = Wallet.makeApiWalletEffect(api) + + return attachCapabilities(c, { + getAddress: effects.address, + getRewardAddress: effects.rewardAddress, + signTx: effects.signTx, + signMessage: effects.signMessage, + walletSubmitTx: effects.submitTx + }) + } diff --git a/packages/evolution/src/sdk/client/attachCapabilities.ts b/packages/evolution/src/sdk/client/attachCapabilities.ts new file mode 100644 index 00000000..b5e037af --- /dev/null +++ b/packages/evolution/src/sdk/client/attachCapabilities.ts @@ -0,0 +1,59 @@ +/** + * Internal helper for building composable client constructors. + * + * Centralizes the boilerplate that every provider/wallet constructor needs: + * - Derives Promise methods from Effect methods via `Effect.runPromise` + * - Derives AsyncIterable methods from Stream methods via `Stream.toAsyncIterable` + * - Merges the Effect namespace + * - Rebinds `newTx` and `with` to the new client object + * + * @internal + * @module + */ + +import { Effect, Stream } from "effect" + +import { type Client, newTx } from "./Client.js" + +/** + * Attach Effect-based capabilities to a client, auto-deriving Promise methods. + * + * Each entry in `effects` becomes: + * - A Promise method on the client: `client.getUtxos(addr)` → `Effect.runPromise(effects.getUtxos(addr))` + * - An entry in `client.Effect`: `client.Effect.getUtxos(addr)` → the raw Effect + * + * Functions returning a `Stream` are auto-detected at call time and derive an + * `AsyncIterable` via `Stream.toAsyncIterable`. `break` in `for await` triggers + * stream cleanup. + * + * Also rebinds `newTx` and `with` to the augmented client. + * + * @internal + */ +export const attachCapabilities = ( + c: T, + effects: Record) => Effect.Effect | Stream.Stream> +): T & Caps => { + const promiseMethods: Record = {} + for (const [key, fn] of Object.entries(effects)) { + promiseMethods[key] = (...args: Array) => { + const result = (fn as (...a: Array) => Effect.Effect | Stream.Stream)(...args) + if (Stream.StreamTypeId in result) { + return Stream.toAsyncIterable(result as Stream.Stream) + } + return Effect.runPromise(result as Effect.Effect) + } + } + + const result: Record = { + ...(c as Record), + ...promiseMethods, + newTx: () => newTx(result as T & Caps), + with: (fn: (c: T & Caps) => R): R => fn(result as T & Caps), + Effect: { + ...(c.Effect as Record), + ...effects, + }, + } + return result as T & Caps +} diff --git a/packages/evolution/src/sdk/client/index.ts b/packages/evolution/src/sdk/client/index.ts index c5da4490..84be2452 100644 --- a/packages/evolution/src/sdk/client/index.ts +++ b/packages/evolution/src/sdk/client/index.ts @@ -1 +1,20 @@ +export * from "./Capabilities.js" export * from "./Client.js" +export { + type ApiWalletClient, + type MinimalClient, + type MinimalClientEffect, + type ProviderConfig, + type ProviderError, + type ProviderOnlyClient, + type ReadOnlyClient, + type ReadOnlyClientEffect, + type ReadOnlyWalletClient, + type RetryConfig, + type RetryPolicy, + RetryPresets, + type SigningClient, + type SigningClientEffect, + type SigningWalletClient, + type WalletConfig, +} from "./ClientLegacy.js" diff --git a/packages/evolution/src/sdk/index.ts b/packages/evolution/src/sdk/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/evolution/src/sdk/provider/Koios.ts b/packages/evolution/src/sdk/provider/Koios.ts index fd1c1c9c..e891d95b 100644 --- a/packages/evolution/src/sdk/provider/Koios.ts +++ b/packages/evolution/src/sdk/provider/Koios.ts @@ -11,7 +11,7 @@ import type { Provider, ProviderEffect } from "./Provider.js" * @since 2.0.0 * @category constructors */ -export class Koios implements Provider { +export class KoiosProvider implements Provider { private readonly baseUrl: string private readonly token?: string diff --git a/packages/evolution/src/sdk/provider/internal/BlockfrostEffect.ts b/packages/evolution/src/sdk/provider/internal/BlockfrostEffect.ts index fd36e9bf..0183456e 100644 --- a/packages/evolution/src/sdk/provider/internal/BlockfrostEffect.ts +++ b/packages/evolution/src/sdk/provider/internal/BlockfrostEffect.ts @@ -27,7 +27,7 @@ import * as CoreUTxO from "../../../UTxO.js" import type * as Provider from "../Provider.js" import { ProviderError } from "../Provider.js" import * as Blockfrost from "./Blockfrost.js" -import * as HttpUtils from "./HttpUtils.js" +import * as HttpUtils from "./Http.js" // ============================================================================ // Rate Limiting Configuration diff --git a/packages/evolution/src/sdk/provider/internal/HttpUtils.ts b/packages/evolution/src/sdk/provider/internal/Http.ts similarity index 100% rename from packages/evolution/src/sdk/provider/internal/HttpUtils.ts rename to packages/evolution/src/sdk/provider/internal/Http.ts diff --git a/packages/evolution/src/sdk/provider/internal/Koios.ts b/packages/evolution/src/sdk/provider/internal/Koios.ts index 098f926c..1d475a34 100644 --- a/packages/evolution/src/sdk/provider/internal/Koios.ts +++ b/packages/evolution/src/sdk/provider/internal/Koios.ts @@ -18,7 +18,7 @@ import * as PlutusV3 from "../../../PlutusV3.js" import type * as Script from "../../../Script.js" import * as TransactionHash from "../../../TransactionHash.js" import * as CoreUTxO from "../../../UTxO.js" -import * as HttpUtils from "./HttpUtils.js" +import * as HttpUtils from "./Http.js" export const ProtocolParametersSchema = Schema.Struct({ pvt_motion_no_confidence: Schema.Number, diff --git a/packages/evolution/src/sdk/provider/internal/KoiosEffect.ts b/packages/evolution/src/sdk/provider/internal/KoiosEffect.ts index 2fe81482..72eb930f 100644 --- a/packages/evolution/src/sdk/provider/internal/KoiosEffect.ts +++ b/packages/evolution/src/sdk/provider/internal/KoiosEffect.ts @@ -18,7 +18,7 @@ import type * as TransactionInput from "../../../TransactionInput.js" import type * as CoreUTxO from "../../../UTxO.js" import type * as EvalRedeemer from "../../EvalRedeemer.js" import * as Provider from "../Provider.js" -import * as HttpUtils from "./HttpUtils.js" +import * as HttpUtils from "./Http.js" import * as _Koios from "./Koios.js" import * as _Ogmios from "./Ogmios.js" diff --git a/packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts b/packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts index 6f8cf427..a32c67f3 100644 --- a/packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts +++ b/packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts @@ -1,5 +1,5 @@ import { FetchHttpClient } from "@effect/platform" -import { Array as _Array, Effect, pipe, Schedule, Schema } from "effect" +import { Array as _Array, Effect, pipe, Schedule, Schema, Stream } from "effect" import * as CoreAddress from "../../../Address.js" import * as CoreAssets from "../../../Assets/index.js" @@ -25,7 +25,7 @@ import type * as TransactionInput from "../../../TransactionInput.js" import * as CoreUTxO from "../../../UTxO.js" import type { EvalRedeemer } from "../../EvalRedeemer.js" import * as Provider from "../Provider.js" -import * as HttpUtils from "./HttpUtils.js" +import * as HttpUtils from "./Http.js" import * as Kupo from "./Kupo.js" import * as Ogmios from "./Ogmios.js" @@ -477,3 +477,40 @@ export const getDatumEffect = (kupoUrl: string, headers?: { kupoHeader?: Record< ) return Schema.decodeSync(PlutusData.FromCBORHex())(result.datum) }) + +export const watchUtxosEffect = (kupoUrl: string, headers?: { kupoHeader?: Record }) => + (addressOrCredential: CoreAddress.Address | Credential.Credential, pollInterval = 2000): Stream.Stream => { + let pattern: string + if (addressOrCredential instanceof CoreAddress.Address) { + const addressStr = CoreAddress.toBech32(addressOrCredential) + pattern = `${kupoUrl}/matches/${addressStr}?unspent` + } else { + pattern = `${kupoUrl}/matches/${addressOrCredential.hash}/*?unspent` + } + const schema = Schema.Array(Kupo.UTxOSchema) + const toUtxos = kupmiosUtxosToUtxos(kupoUrl, headers?.kupoHeader) + + // Track seen UTxO IDs (txHash#index) to only emit new ones + const seen = new Set() + + return Stream.repeatEffectWithSchedule( + pipe( + HttpUtils.get(pattern, schema, headers?.kupoHeader), + Effect.flatMap((u) => toUtxos(u)), + Effect.provide(FetchHttpClient.layer), + Effect.timeout(TIMEOUT), + Effect.catchAll(wrapError("watchUtxos")) + ), + Schedule.spaced(pollInterval) + ).pipe( + Stream.flatMap((utxos) => { + const newUtxos = utxos.filter((utxo) => { + const id = `${TransactionHash.toHex(utxo.transactionId)}#${utxo.index}` + if (seen.has(id)) return false + seen.add(id) + return true + }) + return Stream.fromIterable(newUtxos) + }) + ) + } diff --git a/packages/evolution/src/sdk/provider/internal/MaestroEffect.ts b/packages/evolution/src/sdk/provider/internal/MaestroEffect.ts index 1f0d2bbf..031a2f25 100644 --- a/packages/evolution/src/sdk/provider/internal/MaestroEffect.ts +++ b/packages/evolution/src/sdk/provider/internal/MaestroEffect.ts @@ -18,7 +18,7 @@ import type * as TransactionInput from "../../../TransactionInput.js" import * as TxOut from "../../../TxOut.js" import type * as CoreUTxO from "../../../UTxO.js" import { ProviderError } from "../Provider.js" -import * as HttpUtils from "./HttpUtils.js" +import * as HttpUtils from "./Http.js" import * as Maestro from "./Maestro.js" // ============================================================================ diff --git a/packages/evolution/src/sdk/wallet/Derivation.ts b/packages/evolution/src/sdk/wallet/Derivation.ts index 40e5fdc3..61cfec5a 100644 --- a/packages/evolution/src/sdk/wallet/Derivation.ts +++ b/packages/evolution/src/sdk/wallet/Derivation.ts @@ -41,11 +41,13 @@ export const walletFromSeed = ( password?: string addressType?: "Base" | "Enterprise" accountIndex?: number - network?: "Mainnet" | "Testnet" | "Custom" + paymentIndex?: number + stakeIndex?: number + networkId?: 0 | 1 } = {} ): Effect.Effect => { return Effect.gen(function* () { - const { accountIndex = 0, addressType = "Base", network = "Mainnet" } = options + const { accountIndex = 0, addressType = "Base", networkId = 0, paymentIndex = 0, stakeIndex = 0 } = options const entropy = yield* Effect.try({ try: () => mnemonicToEntropy(seed, English), catch: (cause) => new DerivationError({ message: "Invalid seed phrase", cause }) @@ -53,18 +55,17 @@ export const walletFromSeed = ( const rootXPrv = yield* Bip32PrivateKey.Either.fromBip39Entropy(entropy, options?.password ?? "") const paymentNode = yield* Bip32PrivateKey.Either.derive( rootXPrv, - Bip32PrivateKey.CardanoPath.paymentIndices(accountIndex, 0) + Bip32PrivateKey.CardanoPath.paymentIndices(accountIndex, paymentIndex) ) const stakeNode = yield* Bip32PrivateKey.Either.derive( rootXPrv, - Bip32PrivateKey.CardanoPath.stakeIndices(accountIndex, 0) + Bip32PrivateKey.CardanoPath.stakeIndices(accountIndex, stakeIndex) ) const paymentKey = Bip32PrivateKey.toPrivateKey(paymentNode) const stakeKey = Bip32PrivateKey.toPrivateKey(stakeNode) const paymentKeyHash = KeyHash.fromPrivateKey(paymentKey) const stakeKeyHash = KeyHash.fromPrivateKey(stakeKey) - const networkId = network === "Mainnet" ? 1 : 0 const address: CoreAddress.Address = addressType === "Base" @@ -263,22 +264,18 @@ export function walletFromPrivateKey( options: { stakeKeyBech32?: string addressType?: "Base" | "Enterprise" - network?: "Mainnet" | "Testnet" | "Custom" + networkId?: 0 | 1 } = {} ): Effect.Effect { return Effect.gen(function* () { - const { stakeKeyBech32, addressType = stakeKeyBech32 ? "Base" : "Enterprise", network = "Mainnet" } = options + const { stakeKeyBech32, addressType = stakeKeyBech32 ? "Base" : "Enterprise", networkId = 0 } = options - // Use the Effect-based Either API from PrivateKey module - can yield directly on Either const paymentKey = yield* Effect.mapError( - // PrivateKey.Either.fromBech32(paymentKeyBech32), Schema.decode(PrivateKey.FromBech32)(paymentKeyBech32), (cause) => new DerivationError({ message: cause.message, cause }) ) const paymentKeyHash = KeyHash.fromPrivateKey(paymentKey) - const networkId = network === "Mainnet" ? 1 : 0 - let address: CoreAddress.Address let stakeKey: PrivateKey.PrivateKey | undefined let stakeKeyHash: KeyHash.KeyHash | undefined diff --git a/packages/evolution/src/sdk/wallet/Wallet.ts b/packages/evolution/src/sdk/wallet/Wallet.ts new file mode 100644 index 00000000..55aeac28 --- /dev/null +++ b/packages/evolution/src/sdk/wallet/Wallet.ts @@ -0,0 +1,493 @@ +/** + * @todo Once `ClientImpl.ts` (the legacy client path) is removed, this module + * should be refactored: + * + * - Rename to `Signing.ts` — the core concern is signing logic, not "wallet" as + * an abstraction. + * - Drop the `SigningWallet`, `ReadOnlyWallet`, and `ApiWallet` typed objects + * (with their Promise wrappers and `.type` discriminants) — these only exist + * to serve `ClientImpl.ts`. In the new composable client, capabilities are + * what matter and the wallet-as-object pattern is not needed. + * - Keep: `WalletError`, `Payload`, `SignedMessage`, `WalletApi`, the `*Effect` + * interfaces, and the Effect-only factories (`makeSigningWalletEffect`, etc.). + * - `Wallets.ts` would then import from `Signing.ts` directly. + */ +import { Data, Effect, Equal, ParseResult, Schema } from "effect" + +import * as CoreAddress from "../../Address.js" +import * as Bytes from "../../Bytes.js" +import * as KeyHash from "../../KeyHash.js" +import { COSESign1FromCBORBytes } from "../../message-signing/CoseSign1.js" +import * as MessageSignData from "../../message-signing/SignData.js" +import type * as NativeScripts from "../../NativeScripts.js" +import * as PrivateKey from "../../PrivateKey.js" +import * as CoreRewardAccount from "../../RewardAccount.js" +import * as CoreRewardAddress from "../../RewardAddress.js" +import * as Transaction from "../../Transaction.js" +import * as TransactionHash from "../../TransactionHash.js" +import * as TransactionWitnessSet from "../../TransactionWitnessSet.js" +import { hashTransaction, hashTransactionRaw } from "../../utils/Hash.js" +import * as CoreUTxO from "../../UTxO.js" +import * as VKey from "../../VKey.js" +import type { EffectToPromiseAPI } from "../Type.js" +import * as Derivation from "./Derivation.js" + +/** + * Error class for wallet-related operations. + * + * @since 2.0.0 + * @category errors + */ +export class WalletError extends Data.TaggedError("WalletError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Payload for message signing - either a string or raw bytes. + * + * @since 2.0.0 + * @category model + */ +export type Payload = string | Uint8Array + +/** + * Signed message containing the original payload and its cryptographic signature. + * + * @since 2.0.0 + * @category model + */ +export interface SignedMessage { + readonly payload: Payload + readonly signature: string +} + +/** + * Network identifier for wallet operations. + * + * @since 2.0.0 + * @category model + */ +export type Network = "Mainnet" | "Testnet" | "Custom" + +/** + * Read-only wallet Effect interface. + * + * @since 2.0.0 + * @category model + */ +export interface ReadOnlyWalletEffect { + readonly address: () => Effect.Effect + readonly rewardAddress: () => Effect.Effect +} + +/** + * Read-only wallet interface (Promise + Effect dual API). + * Used by the legacy client path. + * + * @since 2.0.0 + * @category model + */ +export interface ReadOnlyWallet extends EffectToPromiseAPI { + readonly Effect: ReadOnlyWalletEffect + readonly type: "read-only" +} + +/** + * Signing wallet Effect interface. + * + * @since 2.0.0 + * @category model + */ +export interface SigningWalletEffect extends ReadOnlyWalletEffect { + readonly signTx: ( + tx: Transaction.Transaction | string, + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } + ) => Effect.Effect + readonly signMessage: ( + address: CoreAddress.Address | CoreRewardAddress.RewardAddress, + payload: Payload + ) => Effect.Effect +} + +/** + * Signing wallet interface (Promise + Effect dual API). + * Used by the legacy client path. + * + * @since 2.0.0 + * @category model + */ +export interface SigningWallet extends EffectToPromiseAPI { + readonly Effect: SigningWalletEffect + readonly type: "signing" +} + +/** + * CIP-30 compatible wallet API interface. + * + * @since 2.0.0 + * @category model + */ +export interface WalletApi { + getUsedAddresses(): Promise> + getUnusedAddresses(): Promise> + getRewardAddresses(): Promise> + getUtxos(): Promise> + signTx(txCborHex: string, partialSign: boolean): Promise + signData(addressHex: string, payload: Payload): Promise + submitTx(txCborHex: string): Promise +} + +/** + * API wallet Effect interface for CIP-30 compatible wallets. + * + * @since 2.0.0 + * @category model + */ +export interface ApiWalletEffect extends ReadOnlyWalletEffect { + readonly signTx: ( + tx: Transaction.Transaction | string, + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } + ) => Effect.Effect + readonly signMessage: ( + address: CoreAddress.Address | CoreRewardAddress.RewardAddress, + payload: Payload + ) => Effect.Effect + readonly submitTx: ( + tx: Transaction.Transaction | string + ) => Effect.Effect +} + +/** + * API wallet interface (Promise + Effect dual API). + * Used by the legacy client path. + * + * @since 2.0.0 + * @category model + */ +export interface ApiWallet extends EffectToPromiseAPI { + readonly Effect: ApiWalletEffect + readonly api: WalletApi + readonly type: "api" +} + +// ── Private helpers ─────────────────────────────────────────────────────────── + +const toSignDataAddressHex = (address: CoreAddress.Address | CoreRewardAddress.RewardAddress): string => + address instanceof CoreAddress.Address + ? CoreAddress.toHex(address) + : CoreRewardAccount.toHex(CoreRewardAccount.fromBech32(address)) + +const signPayload = ( + address: CoreAddress.Address | CoreRewardAddress.RewardAddress, + payload: Payload, + paymentSigningKeyBech32: string +): SignedMessage => { + const paymentSigningKey = PrivateKey.fromBech32(paymentSigningKeyBech32) + const payloadBytes = typeof payload === "string" ? new TextEncoder().encode(payload) : payload + const signed = MessageSignData.signData(toSignDataAddressHex(address), payloadBytes, paymentSigningKey) + const coseSign1 = Schema.decodeSync(COSESign1FromCBORBytes())(signed.signature) + return { payload, signature: coseSign1.toUserFacingEncoding() } +} + +const extractKeyHashesFromNativeScript = (script: NativeScripts.NativeScriptVariants): Set => { + const keyHashes = new Set() + const traverse = (current: NativeScripts.NativeScriptVariants): void => { + switch (current._tag) { + case "ScriptPubKey": + keyHashes.add(Bytes.toHex(current.keyHash)) + break + case "ScriptAll": + case "ScriptAny": + case "ScriptNOfK": + for (const nested of current.scripts) traverse(nested) + break + case "InvalidBefore": + case "InvalidHereafter": + break + } + } + traverse(script) + return keyHashes +} + +const computeRequiredKeyHashes = (params: { + paymentKhHex?: string + rewardAddress?: CoreRewardAddress.RewardAddress | null + stakeKhHex?: string + tx: Transaction.Transaction + utxos: ReadonlyArray + referenceUtxos?: ReadonlyArray +}): Set => { + const required = new Set() + + if (params.tx.body.requiredSigners) { + for (const keyHash of params.tx.body.requiredSigners) required.add(KeyHash.toHex(keyHash)) + } + + if (params.tx.witnessSet.nativeScripts) { + for (const ns of params.tx.witnessSet.nativeScripts) { + for (const kh of extractKeyHashesFromNativeScript(ns.script)) required.add(kh) + } + } + + if (params.referenceUtxos) { + for (const utxo of params.referenceUtxos) { + if (utxo.scriptRef?._tag === "NativeScript") { + for (const kh of extractKeyHashesFromNativeScript(utxo.scriptRef.script)) required.add(kh) + } + } + } + + const ownedRefs = new Set(params.utxos.map(CoreUTxO.toOutRefString)) + + const checkInputs = (inputs?: ReadonlyArray) => { + if (!inputs || !params.paymentKhHex) return + for (const input of inputs) { + const key = `${TransactionHash.toHex(input.transactionId)}#${Number(input.index)}` + if (ownedRefs.has(key)) required.add(params.paymentKhHex) + } + } + checkInputs(params.tx.body.inputs) + if (params.tx.body.collateralInputs) checkInputs(params.tx.body.collateralInputs) + + if (params.tx.body.withdrawals && params.rewardAddress && params.stakeKhHex) { + const ourReward = Schema.decodeSync(CoreRewardAccount.FromBech32)(params.rewardAddress) + for (const [rewardAccount] of params.tx.body.withdrawals.withdrawals.entries()) { + if (Equal.equals(ourReward, rewardAccount)) { + required.add(params.stakeKhHex) + break + } + } + } + + if (params.tx.body.certificates && params.stakeKhHex) { + for (const cert of params.tx.body.certificates) { + const credential = + cert._tag === "StakeRegistration" || + cert._tag === "StakeDeregistration" || + cert._tag === "StakeDelegation" || + cert._tag === "RegCert" || + cert._tag === "UnregCert" || + cert._tag === "StakeVoteDelegCert" || + cert._tag === "StakeRegDelegCert" || + cert._tag === "StakeVoteRegDelegCert" || + cert._tag === "VoteDelegCert" || + cert._tag === "VoteRegDelegCert" + ? cert.stakeCredential + : undefined + if (credential?._tag === "KeyHash" && KeyHash.toHex(credential) === params.stakeKhHex) { + required.add(params.stakeKhHex) + } + } + } + + return required +} + +/** + * Shared signTx + signMessage Effect implementation built from a derivation result. + * Both seed and private-key wallets use this — the only difference is how derivation is obtained. + */ +const buildSigningWalletEffect = ( + derivationEffect: Effect.Effect +): SigningWalletEffect => ({ + address: () => Effect.map(derivationEffect, (d) => d.address), + rewardAddress: () => Effect.map(derivationEffect, (d) => d.rewardAddress ?? null), + signTx: (txOrHex, context) => + Effect.gen(function* () { + const derivation = yield* derivationEffect + const tx = + typeof txOrHex === "string" + ? yield* ParseResult.decodeUnknownEither(Transaction.FromCBORHex())(txOrHex).pipe( + Effect.mapError((cause) => new WalletError({ message: `Failed to decode transaction: ${cause}`, cause })) + ) + : txOrHex + + const required = computeRequiredKeyHashes({ + paymentKhHex: derivation.paymentKhHex, + rewardAddress: derivation.rewardAddress ?? null, + stakeKhHex: derivation.stakeKhHex, + tx, + utxos: context?.utxos ?? [], + referenceUtxos: context?.referenceUtxos ?? [] + }) + + const txHash = + typeof txOrHex === "string" + ? hashTransactionRaw(Transaction.extractBodyBytes(Bytes.fromHex(txOrHex))) + : hashTransaction(tx.body) + + const witnesses: Array = [] + const seenVKeys = new Set() + for (const keyHash of required) { + const signingKey = derivation.keyStore.get(keyHash) + if (!signingKey) continue + const vk = VKey.fromPrivateKey(signingKey) + const vkHex = VKey.toHex(vk) + if (seenVKeys.has(vkHex)) continue + seenVKeys.add(vkHex) + witnesses.push( + new TransactionWitnessSet.VKeyWitness({ vkey: vk, signature: PrivateKey.sign(signingKey, txHash.hash) }) + ) + } + + return witnesses.length > 0 + ? TransactionWitnessSet.fromVKeyWitnesses(witnesses) + : TransactionWitnessSet.empty() + }), + signMessage: (address, payload) => + Effect.map(derivationEffect, (d) => signPayload(address, payload, d.paymentKey)) +}) + +// ── Effect-only factories (new client API) ──────────────────────────────────── + +/** + * Create a signing wallet Effect interface from a mnemonic seed phrase. + * Returns the Effect interface only — no Promise wrapping. + * + * @since 2.1.0 + * @category constructors + */ +export const makeSigningWalletEffect = ( + networkId: 0 | 1, + seed: string, + options: { + accountIndex?: number + paymentIndex?: number + stakeIndex?: number + addressType?: "Base" | "Enterprise" + password?: string + } = {} +): SigningWalletEffect => { + const derivationEffect = Derivation.walletFromSeed(seed, { ...options, networkId }).pipe( + Effect.mapError((cause) => new WalletError({ message: cause.message, cause })) + ) + return buildSigningWalletEffect(derivationEffect) +} + +/** + * Create a signing wallet Effect interface from a bech32 private key. + * Returns the Effect interface only — no Promise wrapping. + * + * @since 2.1.0 + * @category constructors + */ +export const makePrivateKeyWalletEffect = ( + networkId: 0 | 1, + paymentKey: string, + options: { + stakeKey?: string + addressType?: "Base" | "Enterprise" + } = {} +): SigningWalletEffect => { + const derivationEffect = Derivation.walletFromPrivateKey(paymentKey, { + stakeKeyBech32: options.stakeKey, + addressType: options.addressType, + networkId + }).pipe(Effect.mapError((cause) => new WalletError({ message: cause.message, cause }))) + return buildSigningWalletEffect(derivationEffect) +} + +/** + * Create a CIP-30 API wallet Effect interface. + * Returns the Effect interface only — no Promise wrapping. + * + * @since 2.1.0 + * @category constructors + */ +export const makeApiWalletEffect = (api: WalletApi): ApiWalletEffect => { + let cachedAddress: CoreAddress.Address | null = null + let cachedRewardAddress: CoreRewardAddress.RewardAddress | null = null + let hasLoadedRewardAddress = false + + const getPrimaryAddress = Effect.gen(function* () { + if (cachedAddress) return cachedAddress + const usedAddresses = yield* Effect.tryPromise({ + try: () => api.getUsedAddresses(), + catch: (cause) => new WalletError({ message: (cause as Error).message, cause }) + }) + const unusedAddresses = yield* Effect.tryPromise({ + try: () => api.getUnusedAddresses(), + catch: (cause) => new WalletError({ message: (cause as Error).message, cause }) + }) + const addressString = usedAddresses[0] ?? unusedAddresses[0] + if (!addressString) { + return yield* Effect.fail(new WalletError({ message: "Wallet API returned no addresses", cause: undefined })) + } + try { + cachedAddress = CoreAddress.fromBech32(addressString) + } catch { + try { + cachedAddress = CoreAddress.fromHex(addressString) + } catch (cause) { + return yield* Effect.fail( + new WalletError({ message: `Invalid address format from wallet: ${addressString}`, cause }) + ) + } + } + return cachedAddress + }) + + const getPrimaryRewardAddress = Effect.gen(function* () { + if (hasLoadedRewardAddress) return cachedRewardAddress + const rewardAddresses = yield* Effect.tryPromise({ + try: () => api.getRewardAddresses(), + catch: (cause) => new WalletError({ message: (cause as Error).message, cause }) + }) + cachedRewardAddress = rewardAddresses[0] + ? Schema.decodeSync(CoreRewardAddress.RewardAddress)(rewardAddresses[0]) + : null + hasLoadedRewardAddress = true + return cachedRewardAddress + }) + + return { + address: () => getPrimaryAddress, + rewardAddress: () => getPrimaryRewardAddress, + signTx: (txOrHex) => + Effect.gen(function* () { + const cborHex = typeof txOrHex === "string" ? txOrHex : Transaction.toCBORHex(txOrHex) + const witnessHex = yield* Effect.tryPromise({ + try: () => api.signTx(cborHex, true), + catch: (cause) => new WalletError({ message: "User rejected transaction signing", cause }) + }) + return yield* ParseResult.decodeUnknownEither(TransactionWitnessSet.FromCBORHex())(witnessHex).pipe( + Effect.mapError((cause) => new WalletError({ message: `Failed to decode witness set: ${cause}`, cause })) + ) + }), + signMessage: (address, payload) => + Effect.gen(function* () { + const addressString = address instanceof CoreAddress.Address ? CoreAddress.toBech32(address) : address + const result = yield* Effect.tryPromise({ + try: () => api.signData(addressString, payload), + catch: (cause) => new WalletError({ message: "User rejected message signing", cause }) + }) + return { payload, signature: result.signature } + }), + submitTx: (txOrHex) => + Effect.gen(function* () { + const cborHex = typeof txOrHex === "string" ? txOrHex : Transaction.toCBORHex(txOrHex) + const txHashHex = yield* Effect.tryPromise({ + try: () => api.submitTx(cborHex), + catch: (cause) => new WalletError({ message: (cause as Error).message, cause }) + }) + return Schema.decodeSync(TransactionHash.FromHex)(txHashHex) + }) + } +} + +/** + * Create a read-only wallet Effect interface from a pre-parsed address. + * Returns the Effect interface only — no Promise wrapping. + * + * @since 2.1.0 + * @category constructors + */ +export const makeReadOnlyWalletEffect = ( + address: CoreAddress.Address, + rewardAddress: CoreRewardAddress.RewardAddress | null = null +): ReadOnlyWalletEffect => ({ + address: () => Effect.succeed(address), + rewardAddress: () => Effect.succeed(rewardAddress) +}) diff --git a/packages/evolution/src/sdk/wallet/WalletNew.ts b/packages/evolution/src/sdk/wallet/WalletNew.ts deleted file mode 100644 index cb390c4f..00000000 --- a/packages/evolution/src/sdk/wallet/WalletNew.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Data, type Effect } from "effect" - -import type * as CoreAddress from "../../Address.js" -import type * as RewardAddress from "../../RewardAddress.js" -import type * as Transaction from "../../Transaction.js" -import type * as TransactionHash from "../../TransactionHash.js" -import type * as TransactionWitnessSet from "../../TransactionWitnessSet.js" -import type * as CoreUTxO from "../../UTxO.js" -import type { EffectToPromiseAPI } from "../Type.js" - -/** - * Error class for wallet-related operations. - * Represents failures during wallet address retrieval, transaction signing, or message signing. - * - * @since 2.0.0 - * @category errors - */ -export class WalletError extends Data.TaggedError("WalletError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Payload for message signing - either a string or raw bytes. - * - * @since 2.0.0 - * @category model - */ -export type Payload = string | Uint8Array - -/** - * Signed message containing the original payload and its cryptographic signature. - * - * @since 2.0.0 - * @category model - */ -export interface SignedMessage { - readonly payload: Payload - readonly signature: string -} - -/** - * Network identifier for wallet operations. - * Mainnet for production, Testnet for testing, or Custom for other networks. - * - * @since 2.0.0 - * @category model - */ -export type Network = "Mainnet" | "Testnet" | "Custom" - -/** - * Read-only wallet Effect interface providing access to wallet data without signing capabilities. - * Suitable for read-only applications that need wallet address information. - * - * @since 2.0.0 - * @category model - */ -export interface ReadOnlyWalletEffect { - readonly address: () => Effect.Effect - readonly rewardAddress: () => Effect.Effect -} - -/** - * Read-only wallet interface providing access to wallet data without signing capabilities. - * Wraps ReadOnlyWalletEffect with promise-based API for browser and non-Effect contexts. - * - * @since 2.0.0 - * @category model - */ -export interface ReadOnlyWallet extends EffectToPromiseAPI { - readonly Effect: ReadOnlyWalletEffect - readonly type: "read-only" -} - -/** - * Signing wallet Effect interface extending read-only wallet with transaction and message signing. - * Sign transaction and message operations require wallet authorization. - * - * @since 2.0.0 - * @category model - */ -export interface SigningWalletEffect extends ReadOnlyWalletEffect { - /** - * Sign a transaction given its structured representation. UTxOs required for correctness - * (e.g. to determine required signers) must be supplied by the caller (client) and not - * fetched internally. Reference UTxOs are used to extract required signers from native scripts - * that are used via reference inputs. - */ - readonly signTx: ( - tx: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } - ) => Effect.Effect - readonly signMessage: ( - address: CoreAddress.Address | RewardAddress.RewardAddress, - payload: Payload - ) => Effect.Effect -} - -/** - * Signing wallet interface with full wallet functionality including transaction signing. - * Wraps SigningWalletEffect with promise-based API for browser and non-Effect contexts. - * - * @since 2.0.0 - * @category model - */ -export interface SigningWallet extends EffectToPromiseAPI { - readonly Effect: SigningWalletEffect - readonly type: "signing" -} - -/** - * CIP-30 compatible wallet API interface representing browser wallet extension methods. - * Used by browser-based wallet applications to interact with native wallet extensions. - * - * @since 2.0.0 - * @category model - */ -export interface WalletApi { - getUsedAddresses(): Promise> - getUnusedAddresses(): Promise> - getRewardAddresses(): Promise> - getUtxos(): Promise> - signTx(txCborHex: string, partialSign: boolean): Promise - signData(addressHex: string, payload: Payload): Promise - submitTx(txCborHex: string): Promise -} - -/** - * API Wallet Effect interface for CIP-30 compatible wallets. - * Extends signing capabilities with direct transaction submission through wallet API. - * API wallets handle both signing and submission through the wallet extension. - * - * @since 2.0.0 - * @category model - */ -export interface ApiWalletEffect extends ReadOnlyWalletEffect { - readonly signTx: ( - tx: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } - ) => Effect.Effect - readonly signMessage: ( - address: CoreAddress.Address | RewardAddress.RewardAddress, - payload: Payload - ) => Effect.Effect - /** - * Submit transaction directly through the wallet API. - * API wallets can submit without requiring a separate provider. - */ - readonly submitTx: ( - tx: Transaction.Transaction | string - ) => Effect.Effect -} - -/** - * API Wallet interface for CIP-30 compatible wallets. - * These wallets handle signing and submission internally through the browser extension. - * Wraps ApiWalletEffect with promise-based API for browser contexts. - * - * @since 2.0.0 - * @category model - */ -export interface ApiWallet extends EffectToPromiseAPI { - readonly Effect: ApiWalletEffect - readonly api: WalletApi - readonly type: "api" -} - -/** - * Create a signing wallet from a mnemonic seed phrase. - * Derives wallet keys from seed with optional account index and address type configuration. - * - * @since 2.0.0 - * @category constructors - */ -export declare function makeWalletFromSeed( - network: Network, - seed: string, - options?: { - addressType?: "Base" | "Enterprise" - accountIndex?: number - password?: string - } -): SigningWallet - -/** - * Create a signing wallet from a bech32-encoded private key. - * - * @since 2.0.0 - * @category constructors - */ -export declare function makeWalletFromPrivateKey(network: Network, privateKeyBech32: string): SigningWallet - -/** - * Create an API wallet from a CIP-30 wallet extension. - * Enables interaction with browser-based wallet extensions like Nami or Eternl. - * - * @since 2.0.0 - * @category constructors - */ -export declare function makeWalletFromAPI(api: WalletApi): ApiWallet - -/** - * Create a read-only wallet from a Cardano address. - * Useful for monitoring wallets and read-only operations without signing capability. - * - * @since 2.0.0 - * @category constructors - */ -export declare function makeWalletFromAddress(network: Network, address: CoreAddress.Address): ReadOnlyWallet diff --git a/packages/evolution/src/utils/FeeValidation.ts b/packages/evolution/src/utils/FeeValidation.ts index 64dfec66..13a2b216 100644 --- a/packages/evolution/src/utils/FeeValidation.ts +++ b/packages/evolution/src/utils/FeeValidation.ts @@ -9,8 +9,14 @@ * @category validation */ +import { Data, Effect } from "effect" + +import * as Assets from "../Assets/index.js" +import * as Script from "../Script.js" +import { TransactionBuilderError } from "../sdk/builders/TransactionBuilder.js" import * as Transaction from "../Transaction.js" import type * as TransactionWitnessSet from "../TransactionWitnessSet.js" +import type * as UTxO from "../UTxO.js" /** * Protocol parameters required for fee calculation. @@ -151,3 +157,182 @@ export const assertValidFee = ( ) } } + +// ============================================================================ +// Fee calculation primitives +// ============================================================================ + +/** + * Error raised when a fee calculation fails. + * + * @since 2.0.0 + * @category errors + */ +export class FeeCalculationError extends Data.TaggedError("FeeCalculationError")<{ + readonly message: string + readonly cause?: unknown +}> {} + +/** + * Calculate the CBOR-serialised byte length of a transaction. + * + * @since 2.0.0 + * @category fee-calculation + */ +export const calculateTransactionSize = ( + transaction: Transaction.Transaction +): number => + Transaction.toCBORBytes(transaction).length + +/** + * Calculate the minimum fee from transaction size and protocol parameters. + * + * Formula: `txSize × minFeeCoefficient + minFeeConstant` + * + * @since 2.0.0 + * @category fee-calculation + */ +export const calculateMinimumFee = ( + transactionSizeBytes: number, + protocolParams: { + minFeeCoefficient: bigint + minFeeConstant: bigint + } +): bigint => + BigInt(transactionSizeBytes) * protocolParams.minFeeCoefficient + protocolParams.minFeeConstant + +/** + * Tiered reference-script fee: direct port of the Cardano ledger's + * `tierRefScriptFee` function. + * + * Each `sizeIncrement`-byte chunk is priced at `curTierPrice` per byte, + * then `curTierPrice *= multiplier` for the next chunk. + * Final result: `floor(total)`. + * + * @since 2.0.0 + * @category reference-scripts + */ +export const tierRefScriptFee = ( + multiplier: number, + sizeIncrement: number, + baseFee: number, + totalSize: number +): bigint => { + let acc = 0 + let curTierPrice = baseFee + let remaining = totalSize + + while (remaining >= sizeIncrement) { + acc += sizeIncrement * curTierPrice + curTierPrice *= multiplier + remaining -= sizeIncrement + } + acc += remaining * curTierPrice + + return BigInt(Math.floor(acc)) +} + +/** + * Calculate the total reference-script fee for a set of UTxOs. + * + * Matches the Cardano node's Conway-era rules: + * - Stride: 25,600 bytes + * - Multiplier: 1.2× per tier + * - Base: `minFeeRefScriptCostPerByte` protocol parameter + * - Maximum total script size: 200,000 bytes + * + * Callers must pass both spent inputs and reference inputs, since the node + * sums all `txNonDistinctRefScriptsSize` together. + * + * @since 2.0.0 + * @category reference-scripts + */ +export const calculateReferenceScriptFee = ( + utxos: ReadonlyArray, + costPerByte: number +): Effect.Effect => + Effect.gen(function* () { + let totalScriptSize = 0 + + for (const utxo of utxos) { + if (utxo.scriptRef) { + const scriptBytes = Script.toCBOR(utxo.scriptRef).length + totalScriptSize += scriptBytes + const scriptType = utxo.scriptRef._tag === "NativeScript" ? "Native" : "Plutus" + yield* Effect.logDebug(`[RefScriptFee] ${scriptType} script: ${scriptBytes} bytes`) + } + } + + if (totalScriptSize === 0) return 0n + + yield* Effect.logDebug(`[RefScriptFee] Total reference script size: ${totalScriptSize} bytes`) + + if (totalScriptSize > 200_000) { + return yield* Effect.fail( + new FeeCalculationError({ + message: `Total reference script size (${totalScriptSize} bytes) exceeds maximum limit of 200,000 bytes` + }) + ) + } + + const fee = tierRefScriptFee(1.2, 25_600, costPerByte, totalScriptSize) + yield* Effect.logDebug(`[RefScriptFee] Tiered fee: ${fee} lovelace`) + return fee + }) + +/** + * Validate that transaction inputs cover all outputs plus fee. + * + * Checks lovelace and every native asset unit. Fails with a detailed + * TransactionBuilderError when any required asset is short. + * + * @since 2.0.0 + * @category validation + */ +export const validateTransactionBalance = (params: { + totalInputAssets: Assets.Assets + totalOutputAssets: Assets.Assets + fee: bigint +}): Effect.Effect => + Effect.gen(function* () { + const totalRequired = Assets.withLovelace(params.totalOutputAssets, params.totalOutputAssets.lovelace + params.fee) + + for (const unit of Assets.getUnits(totalRequired)) { + const requiredAmount = Assets.getByUnit(totalRequired, unit) + const availableAmount = Assets.getByUnit(params.totalInputAssets, unit) + + if (availableAmount < requiredAmount) { + const shortfall = requiredAmount - availableAmount + + return yield* Effect.fail( + new TransactionBuilderError({ + message: `Insufficient ${unit}: need ${requiredAmount}, have ${availableAmount} (short by ${shortfall})`, + cause: { + unit, + required: String(requiredAmount), + available: String(availableAmount), + shortfall: String(shortfall) + } + }) + ) + } + } + }) + +/** + * Calculate leftover assets after paying outputs and fee. + * + * Filters out any zero or negative balances from the result. + * + * @since 2.0.0 + * @category fee-calculation + */ +export const calculateLeftoverAssets = (params: { + totalInputAssets: Assets.Assets + totalOutputAssets: Assets.Assets + fee: bigint +}): Assets.Assets => { + const afterOutputs = Assets.subtract(params.totalInputAssets, params.totalOutputAssets) + const leftover = Assets.withLovelace(afterOutputs, afterOutputs.lovelace - params.fee) + return Assets.filter(leftover, (_unit, amount) => amount > 0n) +} diff --git a/packages/evolution/test/TxBuilder.CoinSelectionFailures.test.ts b/packages/evolution/test/TxBuilder.CoinSelectionFailures.test.ts index 03cad89e..de17281c 100644 --- a/packages/evolution/test/TxBuilder.CoinSelectionFailures.test.ts +++ b/packages/evolution/test/TxBuilder.CoinSelectionFailures.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import * as CoreAddress from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" -import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { client, preview } from "../src/index.js" import type * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -19,7 +18,6 @@ const CHANGE_ADDRESS = const RECEIVER_ADDRESS = "addr_test1qpw0djgj0x59ngrjvqthn7enhvruxnsavsw5th63la3mjel3tkc974sr23jmlzgq5zda4gtv8k9cy38756r9y3qgmkqqjz6aa7" -const baseConfig: TxBuilderConfig = {} describe("Insufficient Lovelace", () => { it("should fail when total lovelace is less than payment amount", async () => { @@ -28,7 +26,7 @@ describe("Insufficient Lovelace", () => { createCoreTestUtxo({ transactionId: "a".repeat(64), index: 0, address: CHANGE_ADDRESS, lovelace: 1_000_000n }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(5_000_000n) }) @@ -48,7 +46,7 @@ describe("Insufficient Lovelace", () => { createCoreTestUtxo({ transactionId: "a".repeat(64), index: 0, address: CHANGE_ADDRESS, lovelace: 2_000_000n }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(1_950_000n) }) @@ -72,7 +70,7 @@ describe("Insufficient Lovelace", () => { createCoreTestUtxo({ transactionId: "e".repeat(64), index: 0, address: CHANGE_ADDRESS, lovelace: 100_000n }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(1_000_000n) }) @@ -108,7 +106,7 @@ describe("Missing Native Assets", () => { const paymentAssets = CoreAssets.addByHex(CoreAssets.fromLovelace(2_000_000n), policyB, "546f6b656e42", 100n) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets }) @@ -158,7 +156,7 @@ describe("Missing Native Assets", () => { 10n // Missing token ) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets }) @@ -196,7 +194,7 @@ describe("Insufficient Native Asset Quantity", () => { 100n // Need 100, only have 50 ) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets }) @@ -246,7 +244,7 @@ describe("Insufficient Native Asset Quantity", () => { 100n // Need 100, only have 90 total ) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets }) @@ -293,7 +291,7 @@ describe("Insufficient Native Asset Quantity", () => { 100n // Insufficient ) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets }) @@ -312,7 +310,7 @@ describe("Complex Mixed Failures", () => { it("should fail with empty wallet (no UTxOs)", async () => { const utxos: Array = [] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(1_000_000n) }) @@ -340,7 +338,7 @@ describe("Complex Mixed Failures", () => { }) // 0.001 ADA each ) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(50_000n) }) @@ -370,7 +368,7 @@ describe("Complex Mixed Failures", () => { 1n // Even 1 token will fail ) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets }) @@ -417,7 +415,7 @@ describe("Complex Mixed Failures", () => { 50n // Need 50, have 5 ) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets }) @@ -441,7 +439,7 @@ describe("Edge Case: drainTo Cannot Save Insufficient Funds", () => { createCoreTestUtxo({ transactionId: "a".repeat(64), index: 0n, address: CHANGE_ADDRESS, lovelace: 1_000_000n }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(5_000_000n) // Way more than available }) @@ -463,7 +461,7 @@ describe("Edge Case: drainTo Cannot Save Insufficient Funds", () => { createCoreTestUtxo({ transactionId: "a".repeat(64), index: 0n, address: CHANGE_ADDRESS, lovelace: 800_000n }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) diff --git a/packages/evolution/test/TxBuilder.EdgeCases.test.ts b/packages/evolution/test/TxBuilder.EdgeCases.test.ts index 0501744b..c4a65ceb 100644 --- a/packages/evolution/test/TxBuilder.EdgeCases.test.ts +++ b/packages/evolution/test/TxBuilder.EdgeCases.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import * as Address from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" -import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { client, preview } from "../src/index.js" import type * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -22,7 +21,6 @@ const TESTNET_ADDRESSES = [ const CHANGE_ADDRESS = TESTNET_ADDRESSES[0] const RECEIVER_ADDRESS = TESTNET_ADDRESSES[1] -const baseConfig: TxBuilderConfig = {} describe("TxBuilder P0 Edge Cases - Reselection Loop Boundaries", () => { it("hit max reselection attempts with insufficient funds", async () => { @@ -33,7 +31,7 @@ describe("TxBuilder P0 Edge Cases - Reselection Loop Boundaries", () => { createCoreTestUtxo({ transactionId: "d".repeat(64), index: 0n, address: CHANGE_ADDRESS, lovelace: 100_000n }) ] - const txBuilder = makeTxBuilder(baseConfig) + const txBuilder = client(preview).newTx() // Try to build transaction requiring 5M lovelace (impossible with 400k total) await expect( @@ -145,7 +143,7 @@ describe("TxBuilder P0 Edge Cases - Reselection Loop Boundaries", () => { 100n ) - const txBuilder = makeTxBuilder(baseConfig) + const txBuilder = client(preview).newTx() const signBuilder = await txBuilder .payToAddress({ @@ -205,7 +203,7 @@ describe("TxBuilder P0 Edge Cases - MinUTxO Boundary Precision", () => { createCoreTestUtxo({ transactionId: "b".repeat(64), index: 0n, address: CHANGE_ADDRESS, lovelace: 700_000n }) ] - const txBuilder = makeTxBuilder(baseConfig) + const txBuilder = client(preview).newTx() // Payment that will leave insufficient change with the first UTxO const signBuilder = await txBuilder @@ -285,7 +283,7 @@ describe("TxBuilder P0 Edge Cases - MinUTxO Boundary Precision", () => { }) ] - const txBuilder = makeTxBuilder(baseConfig) + const txBuilder = client(preview).newTx() // Small payment to leave change with max-length asset names const signBuilder = await txBuilder @@ -371,7 +369,7 @@ describe("TxBuilder P0 Edge Cases - MinUTxO Boundary Precision", () => { createCoreTestUtxo({ transactionId: "f".repeat(64), index: 0n, address: CHANGE_ADDRESS, lovelace: 400_000n }) ] - const txBuilder = makeTxBuilder(baseConfig) + const txBuilder = client(preview).newTx() // Payment sized to trigger cascading reselection // Initial 2 UTxOs: 4.4M total diff --git a/packages/evolution/test/TxBuilder.FeeCalculation.test.ts b/packages/evolution/test/TxBuilder.FeeCalculation.test.ts index 770b5b2f..e988080d 100644 --- a/packages/evolution/test/TxBuilder.FeeCalculation.test.ts +++ b/packages/evolution/test/TxBuilder.FeeCalculation.test.ts @@ -7,7 +7,7 @@ import { calculateMinimumFee, tierRefScriptFee, validateTransactionBalance -} from "../src/sdk/builders/TxBuilderImpl.js" +} from "../src/utils/FeeValidation.js" // Test policy IDs (56 hex chars = 28 bytes each) const POLICY1 = "aa".repeat(28) // aaaa...aa (56 chars) diff --git a/packages/evolution/test/TxBuilder.InsufficientChange.test.ts b/packages/evolution/test/TxBuilder.InsufficientChange.test.ts index 5f7f10bf..fed90092 100644 --- a/packages/evolution/test/TxBuilder.InsufficientChange.test.ts +++ b/packages/evolution/test/TxBuilder.InsufficientChange.test.ts @@ -3,9 +3,8 @@ import { FastCheck, Schema } from "effect" import * as Address from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" +import { client, preview } from "../src/index.js" import * as KeyHash from "../src/KeyHash.js" -import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" import * as FeeValidation from "../src/utils/FeeValidation.js" import * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -73,13 +72,12 @@ const assertFeeValid = async ( const createSufficientUtxo = (lovelace: bigint = 100_000_000n): CoreUTxO.UTxO => createCoreTestUtxo({ transactionId: "a".repeat(64), index: 0, address: CHANGE_ADDRESS, lovelace }) -const baseConfig: TxBuilderConfig = {} describe("Fallback Tier 3: onInsufficientChange Strategy", () => { it("should throw error by default when change is insufficient (safe default)", async () => { // Arrange: UTxO with insufficient leftover for change output const utxo = createMinimalUtxo() - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -98,7 +96,7 @@ describe("Fallback Tier 3: onInsufficientChange Strategy", () => { it("should burn leftover as extra fee when onInsufficientChange='burn'", async () => { // Arrange: Same insufficient leftover scenario const utxo = createMinimalUtxo() - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -137,7 +135,7 @@ describe("Fallback Precedence: drainTo before onInsufficientChange", () => { it("should use drainTo (Fallback #1) before checking onInsufficientChange (Fallback #2)", async () => { // Arrange: Insufficient change + both fallbacks configured const utxo = createMinimalUtxo() - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -174,7 +172,7 @@ describe("Normal Path: Sufficient Change (No Fallbacks)", () => { it("should create change output when sufficient funds available", async () => { // Arrange: UTxO with plenty of ADA const utxo = createSufficientUtxo(100_000_000n) // 100 ADA - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(10_000_000n) // 10 ADA payment }) @@ -211,7 +209,7 @@ describe("Normal Path: Sufficient Change (No Fallbacks)", () => { it("should handle exact amount with drainTo without triggering fallbacks", async () => { // Arrange: UTxO with exact amount needed const utxo = createMinimalUtxo() - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -259,7 +257,7 @@ describe("Edge Cases", () => { }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -292,7 +290,7 @@ describe("Edge Cases", () => { // Arrange: Use the standard minimal UTxO (sufficient for tests) const utxo = createMinimalUtxo() - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -357,7 +355,7 @@ describe("Multi-Asset minUTxO Calculation", () => { // Send most lovelace but keep all native assets // This creates leftover with: small lovelace + 10 assets - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_500_000n) // Send 2.5 ADA only }) @@ -433,7 +431,7 @@ describe("Fee Validation: Multiple Witnesses Edge Case", () => { ) // Build transaction that will select all 10 inputs - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(45_000_000n) // 45 ADA }) @@ -488,7 +486,7 @@ describe("Fee Validation: Multiple Witnesses Edge Case", () => { ) } - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(45_000_000n) }) diff --git a/packages/evolution/test/TxBuilder.MinUtxoLovelace.test.ts b/packages/evolution/test/TxBuilder.MinUtxoLovelace.test.ts index 837aaf78..940be901 100644 --- a/packages/evolution/test/TxBuilder.MinUtxoLovelace.test.ts +++ b/packages/evolution/test/TxBuilder.MinUtxoLovelace.test.ts @@ -4,7 +4,7 @@ import { Effect } from "effect" import * as Address from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" import * as PlutusV3 from "../src/PlutusV3.js" -import { calculateMinimumUtxoLovelace } from "../src/sdk/builders/TxBuilderImpl.js" +import { calculateMinimumUtxoLovelace } from "../src/sdk/builders/internal/TxOutput.js" const TEST_ADDRESS = Address.fromBech32( "addr_test1qpw0djgj0x59ngrjvqthn7enhvruxnsavsw5th63la3mjel3tkc974sr23jmlzgq5zda4gtv8k9cy38756r9y3qgmkqqjz6aa7" diff --git a/packages/evolution/test/TxBuilder.Mint.test.ts b/packages/evolution/test/TxBuilder.Mint.test.ts index d5cd47d9..cf04f2e3 100644 --- a/packages/evolution/test/TxBuilder.Mint.test.ts +++ b/packages/evolution/test/TxBuilder.Mint.test.ts @@ -1,15 +1,13 @@ import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" import * as CoreAddress from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" +import { client, preview } from "../src/index.js" import * as Mint from "../src/Mint.js" import * as NativeScripts from "../src/NativeScripts.js" import * as ScriptHash from "../src/ScriptHash.js" -import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" -import { calculateTransactionSize } from "../src/sdk/builders/TxBuilderImpl.js" import * as Text from "../src/Text.js" +import { calculateTransactionSize } from "../src/utils/FeeValidation.js" import * as FeeValidation from "../src/utils/FeeValidation.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -23,7 +21,6 @@ const PROTOCOL_PARAMS = { const CHANGE_ADDRESS = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae" -const baseConfig: TxBuilderConfig = {} // Create a native script for minting const createNativeScript = () => { @@ -46,7 +43,7 @@ describe("TxBuilder Mint", () => { const assetNameHex = Text.toHex("TestToken") const unit = policyId + assetNameHex - const signBuilder = await makeTxBuilder(baseConfig) + const signBuilder = await client(preview).newTx() .attachScript({ script: nativeScript }) .mintAssets({ assets: CoreAssets.fromRecord({ [unit]: 1000n }) @@ -86,7 +83,7 @@ describe("TxBuilder Mint", () => { expect(validation.isValid).toBe(true) expect(validation.difference).toBeGreaterThanOrEqual(0n) // Fee can be overpaid - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) // Strict output expectations @@ -110,7 +107,7 @@ describe("TxBuilder Mint", () => { const unit1 = policyId + assetName1Hex const unit2 = policyId + assetName2Hex - const signBuilder = await makeTxBuilder(baseConfig) + const signBuilder = await client(preview).newTx() .attachScript({ script: nativeScript }) .mintAssets({ assets: CoreAssets.fromRecord({ [unit1]: 100n }) @@ -154,7 +151,7 @@ describe("TxBuilder Mint", () => { expect(validation.isValid).toBe(true) expect(validation.difference).toBe(0n) - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) // Strict output expectations diff --git a/packages/evolution/test/TxBuilder.Reselection.test.ts b/packages/evolution/test/TxBuilder.Reselection.test.ts index 34768a2c..30d1f622 100644 --- a/packages/evolution/test/TxBuilder.Reselection.test.ts +++ b/packages/evolution/test/TxBuilder.Reselection.test.ts @@ -1,12 +1,11 @@ import { describe, expect, it } from "@effect/vitest" -import { Effect, FastCheck, Schema } from "effect" +import { FastCheck, Schema } from "effect" import * as CoreAddress from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" +import { client, preview } from "../src/index.js" import * as KeyHash from "../src/KeyHash.js" -import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" -import { calculateTransactionSize } from "../src/sdk/builders/TxBuilderImpl.js" +import { calculateTransactionSize } from "../src/utils/FeeValidation.js" import * as FeeValidation from "../src/utils/FeeValidation.js" import * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -36,10 +35,6 @@ describe("TxBuilder Re-selection Loop", () => { const CHANGE_ADDRESS = TESTNET_ADDRESSES[0] const RECEIVER_ADDRESS = TESTNET_ADDRESSES[1] - const baseConfig: TxBuilderConfig = { - // No wallet/provider - using manual mode - // changeAddress and availableUtxos provided via build options - } // ============================================================================ // Test Utilities @@ -71,7 +66,7 @@ describe("TxBuilder Re-selection Loop", () => { lovelace: 10_000_000n }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -89,7 +84,7 @@ describe("TxBuilder Re-selection Loop", () => { expect(tx.body.outputs.length).toBe(2) // Payment + change const validation = await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) // Strict expectations with deterministic values @@ -125,7 +120,7 @@ describe("TxBuilder Re-selection Loop", () => { lovelace: 1_000_000n }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) // 2 ADA payment }) @@ -146,7 +141,7 @@ describe("TxBuilder Re-selection Loop", () => { expect(tx.body.outputs.length).toBe(2) const validation = await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) expect(size).toBe(326) // 2 inputs, 1 witness, 2 outputs (Shelley format saves 4 bytes) @@ -165,7 +160,7 @@ describe("TxBuilder Re-selection Loop", () => { lovelace: 1_000_000n }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) // Requesting 2 ADA }) @@ -192,7 +187,7 @@ describe("TxBuilder Re-selection Loop", () => { lovelace: exactAmount }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(paymentAmount) }) @@ -211,7 +206,7 @@ describe("TxBuilder Re-selection Loop", () => { expect(tx.body.outputs.length).toBe(1) const validation = await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) // Strict expectations with deterministic values @@ -240,7 +235,7 @@ describe("TxBuilder Re-selection Loop", () => { }) const utxoWithTokens = new CoreUTxO.UTxO({ ...utxo, assets }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), // Payment leaves leftover + token // 3_000_000 - 2_000_000 - fee(~170k) = ~830k leftover + token @@ -298,7 +293,7 @@ describe("TxBuilder Re-selection Loop", () => { // Pay 2 ADA + 50 tokens let paymentAssets = CoreAssets.fromLovelace(2_000_000n) paymentAssets = CoreAssets.addByHex(paymentAssets, TOKEN_POLICY, TOKEN_NAME, 50n) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets }) @@ -322,7 +317,7 @@ describe("TxBuilder Re-selection Loop", () => { expect(tx.body.outputs[1]).toBeDefined() const validation = await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) // Strict expectations with deterministic values @@ -358,15 +353,11 @@ describe("TxBuilder Re-selection Loop", () => { }) const utxo2 = new CoreUTxO.UTxO({ ...utxo2Base, assets: assets2 }) - // Config with both utxos available for automatic selection - const builderConfig: TxBuilderConfig = { - ...baseConfig - } // Payment requires tokens that utxo1 doesn't have let paymentAssets = CoreAssets.fromLovelace(2_000_000n) paymentAssets = CoreAssets.addByHex(paymentAssets, TOKEN_POLICY, TOKEN_NAME, 100n) - const builder = makeTxBuilder(builderConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: paymentAssets // Requires tokens! }) @@ -410,7 +401,7 @@ describe("TxBuilder Re-selection Loop", () => { }) ) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(5_000_000n) // 5 ADA }) @@ -422,7 +413,7 @@ describe("TxBuilder Re-selection Loop", () => { }) const txWithFakeWitnesses = await signBuilder.toTransactionWithFakeWitnesses() - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) const validation = await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) @@ -456,7 +447,7 @@ describe("TxBuilder Re-selection Loop", () => { lovelace: 5_000_000n }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(6_000_000n) }) @@ -468,7 +459,7 @@ describe("TxBuilder Re-selection Loop", () => { }) const txWithFakeWitnesses = await signBuilder.toTransactionWithFakeWitnesses() - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) const validation = await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) @@ -515,7 +506,7 @@ describe("TxBuilder Re-selection Loop", () => { }) }) - const builder = makeTxBuilder({ ...baseConfig }).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), // Request 280M to force selection of 140+ UTxOs (each 2M), which will create 140+ witnesses // This will exceed the 16KB transaction size limit @@ -557,7 +548,7 @@ describe("TxBuilder Re-selection Loop", () => { createCoreTestUtxo({ transactionId: "3".repeat(64), index: 0, address: CHANGE_ADDRESS, lovelace: 400_000n }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(2_500_000n) // 2.5 ADA payment }) @@ -575,7 +566,7 @@ describe("TxBuilder Re-selection Loop", () => { // Verify transaction is valid await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) // Largest-first picks 1.5M + 1.2M = 2.7M initially (for 2.5M payment) @@ -616,7 +607,7 @@ describe("TxBuilder Re-selection Loop", () => { // Request a payment that will require multiple UTxOs // Each UTxO contributes 350K, minus ~2K fee overhead = ~348K net // To get 3M payment, need ~9 UTxOs initially, but fee will increase - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(3_000_000n) // 3 ADA }) @@ -632,7 +623,7 @@ describe("TxBuilder Re-selection Loop", () => { // Verify transaction is valid const validation = await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) // Should have selected many inputs due to small UTxO sizes @@ -665,7 +656,7 @@ describe("TxBuilder Re-selection Loop", () => { createCoreTestUtxo({ transactionId: "f".repeat(64), index: 0, address: CHANGE_ADDRESS, lovelace: 400_000n }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(2_500_000n) // 2.5 ADA - requires reselection }) @@ -680,7 +671,7 @@ describe("TxBuilder Re-selection Loop", () => { // Verify transaction is valid await assertFeeValid(txWithFakeWitnesses, PROTOCOL_PARAMS) - const size = await Effect.runPromise(calculateTransactionSize(txWithFakeWitnesses)) + const size = calculateTransactionSize(txWithFakeWitnesses) expect(size).toBeLessThanOrEqual(PROTOCOL_PARAMS.maxTxSize) // Should need at least 2 inputs (1.5M + 0.8M + fee > 2.5M) @@ -705,10 +696,6 @@ describe("TxBuilder Reselection After Change", () => { const RECEIVER_ADDRESS = "addr_test1qpw0djgj0x59ngrjvqthn7enhvruxnsavsw5th63la3mjel3tkc974sr23jmlzgq5zda4gtv8k9cy38756r9y3qgmkqqjz6aa7" - const baseConfig: TxBuilderConfig = { - // No wallet/provider - using manual mode - // changeAddress and availableUtxos provided via build options - } /** * Verifies that fee calculation includes the change output in the transaction structure. @@ -722,7 +709,7 @@ describe("TxBuilder Reselection After Change", () => { lovelace: 10_000_000n // 10 ADA }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(5_000_000n) // 5 ADA payment }) @@ -777,7 +764,7 @@ describe("TxBuilder Reselection After Change", () => { lovelace: 2_000_000n }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(3_500_000n) // Needs 2 UTxOs }) @@ -819,7 +806,7 @@ describe("TxBuilder Reselection After Change", () => { }) const utxoWithAssets = new CoreUTxO.UTxO({ ...utxoBase, assets }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(3_000_000n) // Send only lovelace }) @@ -869,7 +856,7 @@ describe("TxBuilder Reselection After Change", () => { lovelace: 2_400_000n }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(RECEIVER_ADDRESS), assets: CoreAssets.fromLovelace(3_000_000n) }) diff --git a/packages/evolution/test/TxBuilder.SendAll.test.ts b/packages/evolution/test/TxBuilder.SendAll.test.ts index 5241e7ca..c334c239 100644 --- a/packages/evolution/test/TxBuilder.SendAll.test.ts +++ b/packages/evolution/test/TxBuilder.SendAll.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import * as Address from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" -import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { client, preview } from "../src/index.js" import type * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -27,7 +26,6 @@ const NFT_POLICY = "c".repeat(56) const HOSKY_NAME_HEX = "484f534b59" // "HOSKY" in hex const NFT_NAME_HEX = "4e4654303031" // "NFT001" in hex -const baseConfig: TxBuilderConfig = {} describe("TxBuilder SendAll", () => { describe("Basic SendAll Operation", () => { @@ -48,7 +46,7 @@ describe("TxBuilder SendAll", () => { ] const totalLovelace = 350_000_000n - const signBuilder = await makeTxBuilder(baseConfig) + const signBuilder = await client(preview).newTx() .sendAll({ to: Address.fromBech32(DESTINATION_ADDRESS) }) .build({ changeAddress: Address.fromBech32(SOURCE_ADDRESS), @@ -100,7 +98,7 @@ describe("TxBuilder SendAll", () => { ] const totalLovelace = 175_000_000n - const signBuilder = await makeTxBuilder(baseConfig) + const signBuilder = await client(preview).newTx() .sendAll({ to: Address.fromBech32(DESTINATION_ADDRESS) }) .build({ changeAddress: Address.fromBech32(SOURCE_ADDRESS), @@ -142,7 +140,7 @@ describe("TxBuilder SendAll", () => { ] await expect( - makeTxBuilder(baseConfig) + client(preview).newTx() .payToAddress({ address: Address.fromBech32(DESTINATION_ADDRESS), assets: CoreAssets.fromLovelace(1_000_000n) @@ -167,7 +165,7 @@ describe("TxBuilder SendAll", () => { ] await expect( - makeTxBuilder(baseConfig) + client(preview).newTx() .collectFrom({ inputs: [utxos[0]] }) .sendAll({ to: Address.fromBech32(DESTINATION_ADDRESS) }) .build({ @@ -182,7 +180,7 @@ describe("TxBuilder SendAll", () => { describe("Insufficient Funds Handling", () => { it("should fail when wallet is empty", async () => { await expect( - makeTxBuilder(baseConfig) + client(preview).newTx() .sendAll({ to: Address.fromBech32(DESTINATION_ADDRESS) }) .build({ changeAddress: Address.fromBech32(SOURCE_ADDRESS), @@ -210,7 +208,7 @@ describe("TxBuilder SendAll", () => { }) ] - const signBuilder = await makeTxBuilder(baseConfig) + const signBuilder = await client(preview).newTx() .sendAll({ to: Address.fromBech32(DESTINATION_ADDRESS) }) .build({ changeAddress: Address.fromBech32(SOURCE_ADDRESS), diff --git a/packages/evolution/test/TxBuilder.SlotConfig.test.ts b/packages/evolution/test/TxBuilder.SlotConfig.test.ts index f551571d..5f2a9dac 100644 --- a/packages/evolution/test/TxBuilder.SlotConfig.test.ts +++ b/packages/evolution/test/TxBuilder.SlotConfig.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest" import * as Address from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { client, mainnet, newTx, preprod, preview } from "../src/index.js" import * as Time from "../src/Time/index.js" import { SLOT_CONFIG_NETWORK } from "../src/Time/SlotConfig.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -30,8 +30,8 @@ const utxos = [ const FIXED_TIME = 1710300000000n // March 13, 2024 describe("TxBuilder slot config resolution", () => { - it("uses Preview slot config when network is Preview", async () => { - const builder = makeTxBuilder({ network: "Preview" }) + it("uses Preview slot config when chain is preview", async () => { + const builder = newTx(client(preview)) const result = await builder .payToAddress({ address: RECEIVER_ADDRESS, assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -50,8 +50,8 @@ describe("TxBuilder slot config resolution", () => { expect(tx.body.ttl).toBe(expectedTTL) }) - it("uses Preprod slot config when network is Preprod", async () => { - const builder = makeTxBuilder({ network: "Preprod" }) + it("uses Preprod slot config when chain is preprod", async () => { + const builder = newTx(client(preprod)) const result = await builder .payToAddress({ address: RECEIVER_ADDRESS, assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -70,8 +70,8 @@ describe("TxBuilder slot config resolution", () => { expect(tx.body.ttl).toBe(expectedTTL) }) - it("uses Mainnet slot config when network is Mainnet", async () => { - const builder = makeTxBuilder({ network: "Mainnet" }) + it("uses Mainnet slot config when chain is mainnet", async () => { + const builder = newTx(client(mainnet)) const result = await builder .payToAddress({ address: RECEIVER_ADDRESS, assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -90,23 +90,6 @@ describe("TxBuilder slot config resolution", () => { expect(tx.body.ttl).toBe(expectedTTL) }) - it("defaults to Mainnet when network is unset", async () => { - const builder = makeTxBuilder({}) - - const result = await builder - .payToAddress({ address: RECEIVER_ADDRESS, assets: CoreAssets.fromLovelace(2_000_000n) }) - .setValidity({ from: FIXED_TIME, to: FIXED_TIME + 300_000n }) - .build({ - changeAddress: CHANGE_ADDRESS, - availableUtxos: utxos, - protocolParameters: PROTOCOL_PARAMS - }) - - const tx = await result.toTransaction() - const expectedStart = Time.unixTimeToSlot(FIXED_TIME, SLOT_CONFIG_NETWORK.Mainnet) - expect(tx.body.validityIntervalStart).toBe(expectedStart) - }) - it("Preview and Mainnet produce different slots for the same timestamp", () => { const previewSlot = Time.unixTimeToSlot(FIXED_TIME, SLOT_CONFIG_NETWORK.Preview) const mainnetSlot = Time.unixTimeToSlot(FIXED_TIME, SLOT_CONFIG_NETWORK.Mainnet) diff --git a/packages/evolution/test/TxBuilder.UnfrackChangeHandling.test.ts b/packages/evolution/test/TxBuilder.UnfrackChangeHandling.test.ts index d59b3bfd..8aa022ed 100644 --- a/packages/evolution/test/TxBuilder.UnfrackChangeHandling.test.ts +++ b/packages/evolution/test/TxBuilder.UnfrackChangeHandling.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import * as CoreAddress from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { client, preview } from "../src/index.js" import * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -60,7 +60,7 @@ describe("TxBuilder: Unfrack Change Handling Integration", () => { }) ] - const builder = makeTxBuilder({}) + const builder = client(preview).newTx() .collectFrom({ inputs: [initialUtxo] }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -134,7 +134,7 @@ describe("TxBuilder: Unfrack Change Handling Integration", () => { createCoreTestUtxo({ transactionId: "e".repeat(64), index: 0, address: CHANGE_ADDRESS, lovelace: 100_000n }) ] - const builder = makeTxBuilder({}) + const builder = client(preview).newTx() .collectFrom({ inputs: [initialUtxo] }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -201,7 +201,7 @@ describe("TxBuilder: Unfrack Change Handling Integration", () => { }) const initialUtxo = new CoreUTxO.UTxO({ ...initialUtxoBase, assets: initialAssets }) - const builder = makeTxBuilder({}) + const builder = client(preview).newTx() .collectFrom({ inputs: [initialUtxo] }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -239,7 +239,7 @@ describe("TxBuilder: Unfrack Change Handling Integration", () => { }) const initialUtxo = new CoreUTxO.UTxO({ ...initialUtxoBase, assets: initialAssets }) - const builder = makeTxBuilder({}) + const builder = client(preview).newTx() .collectFrom({ inputs: [initialUtxo] }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -279,7 +279,7 @@ describe("TxBuilder: Unfrack Change Handling Integration", () => { }) const initialUtxo = new CoreUTxO.UTxO({ ...initialUtxoBase, assets: initialAssets }) - const builder = makeTxBuilder({}) + const builder = client(preview).newTx() .collectFrom({ inputs: [initialUtxo] }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -314,7 +314,7 @@ describe("TxBuilder: Unfrack Change Handling Integration", () => { lovelace: 350_000n }) - const builder = makeTxBuilder({}) + const builder = client(preview).newTx() .collectFrom({ inputs: [initialUtxo] }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -351,7 +351,7 @@ describe("TxBuilder: Unfrack Change Handling Integration", () => { lovelace: 350_000n }) - const builder = makeTxBuilder({}) + const builder = client(preview).newTx() .collectFrom({ inputs: [initialUtxo] }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), diff --git a/packages/evolution/test/TxBuilder.UnfrackDrain.test.ts b/packages/evolution/test/TxBuilder.UnfrackDrain.test.ts index 6fd88279..c292f259 100644 --- a/packages/evolution/test/TxBuilder.UnfrackDrain.test.ts +++ b/packages/evolution/test/TxBuilder.UnfrackDrain.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import * as CoreAddress from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" -import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { client, preview } from "../src/index.js" import * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -130,7 +129,6 @@ const createSimpleAdaWallet = (): Array => [ // ============================================================================ describe("TxBuilder Unfrack + DrainTo Integration", () => { - const baseConfig: TxBuilderConfig = {} // ========================================================================== // Basic Combination Tests @@ -142,7 +140,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Simple ADA-only wallet const utxos = createSimpleAdaWallet() - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), assets: CoreAssets.fromLovelace(1_000_000n) // 1 ADA minimum payment }) @@ -197,7 +195,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { }) ] - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), assets: CoreAssets.fromLovelace(1_000_000n) }) @@ -242,7 +240,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Fragmented wallet with tokens scattered across UTxOs const utxos = createFragmentedWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -281,7 +279,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Wallet with both fungible tokens and NFTs const utxos = createFragmentedWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -321,7 +319,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Wallet with NFTs from multiple policies const utxos = createFragmentedWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -361,7 +359,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Complete fragmented wallet const utxos = createFragmentedWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -413,7 +411,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Simple ADA-only wallet for deterministic drainTo test const utxos = createSimpleAdaWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -452,7 +450,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: ADA-only wallet const utxos = createSimpleAdaWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -506,7 +504,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { }) ] - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -542,7 +540,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Wallet with tokens const utxos = createFragmentedWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -592,7 +590,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { }) ] - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -646,7 +644,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Heavily fragmented wallet (simulating long-term usage) const utxos = createFragmentedWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), @@ -691,7 +689,7 @@ describe("TxBuilder Unfrack + DrainTo Integration", () => { // Arrange: Complete wallet to migrate const utxos = createFragmentedWallet() - const builder = makeTxBuilder(baseConfig) + const builder = client(preview).newTx() .collectFrom({ inputs: utxos }) .payToAddress({ address: CoreAddress.fromBech32(DESTINATION_ADDRESS), diff --git a/packages/evolution/test/TxBuilder.UnfrackMinUTxO.test.ts b/packages/evolution/test/TxBuilder.UnfrackMinUTxO.test.ts index 5499b551..f969f3b8 100644 --- a/packages/evolution/test/TxBuilder.UnfrackMinUTxO.test.ts +++ b/packages/evolution/test/TxBuilder.UnfrackMinUTxO.test.ts @@ -2,8 +2,7 @@ import { describe, expect, it } from "@effect/vitest" import * as Address from "../src/Address.js" import * as CoreAssets from "../src/Assets/index.js" -import type { TxBuilderConfig } from "../src/sdk/builders/TransactionBuilder.js" -import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { client, preview } from "../src/index.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" // Test configuration @@ -47,7 +46,6 @@ const POLICY_ID = "a".repeat(56) // Valid policy ID length const ASSET_NAME_HEX = "544f4b454e" // "TOKEN" in hex describe.concurrent("TxBuilder - Unfrack MinUTxO", () => { - const baseConfig: TxBuilderConfig = {} /** * Validates reselection triggers when leftover has native assets @@ -76,7 +74,7 @@ describe.concurrent("TxBuilder - Unfrack MinUTxO", () => { lovelace: 1_500_000n // 1.5 ADA - provides additional lovelace for unfrack minUTxO }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) // 2.0 ADA only }) @@ -171,7 +169,7 @@ describe.concurrent("TxBuilder - Unfrack MinUTxO", () => { lovelace: 2_000_000n // 2.0 ADA - Extra lovelace to satisfy 3-bundle minUTxO (15 tokens / bundleSize=5) }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -233,7 +231,7 @@ describe.concurrent("TxBuilder - Unfrack MinUTxO", () => { } }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) @@ -279,7 +277,7 @@ describe.concurrent("TxBuilder - Unfrack MinUTxO", () => { } }) - const builder = makeTxBuilder(baseConfig).payToAddress({ + const builder = client(preview).newTx().payToAddress({ address: Address.fromBech32(RECIPIENT_ADDRESS), assets: CoreAssets.fromLovelace(2_000_000n) }) diff --git a/packages/evolution/test/Wallet.test.ts b/packages/evolution/test/Wallet.test.ts new file mode 100644 index 00000000..77cec8ae --- /dev/null +++ b/packages/evolution/test/Wallet.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" + +import * as Address from "../src/Address.js" +import * as Derivation from "../src/sdk/wallet/Derivation.js" +import * as Wallet from "../src/sdk/wallet/Wallet.js" + +const SEED_PHRASE = + "zebra short room flavor rival capital fortune hip profit trust melody office depend adapt visa cycle february link tornado whisper physical kiwi film voyage" + +describe("Wallet runtime constructors", () => { + it.effect("makePrivateKeyWalletEffect respects stake key and address type", () => + Effect.gen(function* () { + const derived = yield* Derivation.walletFromSeed(SEED_PHRASE, { + addressType: "Base", + accountIndex: 0, + networkId: 1 + }) + + const baseEffects = Wallet.makePrivateKeyWalletEffect(1, derived.paymentKey, { + stakeKey: derived.stakeKey, + addressType: "Base" + }) + const enterpriseEffects = Wallet.makePrivateKeyWalletEffect(1, derived.paymentKey, { + addressType: "Enterprise" + }) + + const baseAddress = yield* baseEffects.address() + const baseReward = yield* baseEffects.rewardAddress() + const enterpriseAddress = yield* enterpriseEffects.address() + const enterpriseReward = yield* enterpriseEffects.rewardAddress() + + expect(Address.toBech32(baseAddress)).toBe(Address.toBech32(derived.address)) + expect(baseReward).toBe(derived.rewardAddress) + expect(Address.toBech32(enterpriseAddress)).toBe("addr1v98wl3hnya9l94rt58ky533deyqe9t8zz5n9su26k8e5g2srcn4hd") + expect(enterpriseReward).toBeNull() + }) + ) + + it.effect("makeSigningWalletEffect.signMessage returns a real cms_ signature", () => + Effect.gen(function* () { + const effects = Wallet.makeSigningWalletEffect(1, SEED_PHRASE) + const address = yield* effects.address() + const signed = yield* effects.signMessage(address, "hello world") + + expect(signed.payload).toBe("hello world") + expect(signed.signature.startsWith("cms_")).toBe(true) + }) + ) +}) diff --git a/packages/evolution/test/WalletFromSeed.test.ts b/packages/evolution/test/WalletFromSeed.test.ts index c73a2971..5e17848a 100644 --- a/packages/evolution/test/WalletFromSeed.test.ts +++ b/packages/evolution/test/WalletFromSeed.test.ts @@ -13,7 +13,7 @@ describe("WalletFromSeed", () => { const result1 = yield* walletFromSeed(seedPhrase, { addressType: "Base", accountIndex: 0, - network: "Mainnet" + networkId: 1 }) expect(Address.toBech32(result1.address)).toBe( @@ -27,7 +27,7 @@ describe("WalletFromSeed", () => { "ed25519e_sk19q4d6fguvncszk6f46fvvep5y5w3877y77t3n3dc446wgja25dg968hm8jxkc9d7p982uls6k8uq0srs69e44lay43hxmdx4nc3rttsn0h2f5" ) - const result2 = yield* walletFromSeed(seedPhrase) + const result2 = yield* walletFromSeed(seedPhrase, { networkId: 1 }) expect(Address.toBech32(result2.address)).toBe(Address.toBech32(result1.address)) expect(result2.rewardAddress).toBe(result1.rewardAddress) expect(result2.paymentKey).toBe(result1.paymentKey) @@ -40,7 +40,7 @@ describe("WalletFromSeed", () => { const result1 = yield* walletFromSeed(seedPhrase, { addressType: "Base", accountIndex: 1, - network: "Mainnet" + networkId: 1 }) expect(Address.toBech32(result1.address)).toBe( @@ -55,7 +55,8 @@ describe("WalletFromSeed", () => { ) const result2 = yield* walletFromSeed(seedPhrase, { - accountIndex: 1 + accountIndex: 1, + networkId: 1 }) expect(Address.toBech32(result2.address)).toBe(Address.toBech32(result1.address)) expect(result2.rewardAddress).toBe(result1.rewardAddress) @@ -69,7 +70,7 @@ describe("WalletFromSeed", () => { const result1 = yield* walletFromSeed(seedPhrase, { addressType: "Base", accountIndex: 0, - network: "Custom" + networkId: 0 }) expect(Address.toBech32(result1.address)).toBe( @@ -84,7 +85,7 @@ describe("WalletFromSeed", () => { ) const result2 = yield* walletFromSeed(seedPhrase, { - network: "Custom" + networkId: 0 }) expect(Address.toBech32(result2.address)).toBe(Address.toBech32(result1.address)) expect(result2.rewardAddress).toBe(result1.rewardAddress) @@ -98,7 +99,7 @@ describe("WalletFromSeed", () => { const result1 = yield* walletFromSeed(seedPhrase, { addressType: "Enterprise", accountIndex: 0, - network: "Mainnet" + networkId: 1 }) expect(Address.toBech32(result1.address)).toBe("addr1v98wl3hnya9l94rt58ky533deyqe9t8zz5n9su26k8e5g2srcn4hd") @@ -109,7 +110,8 @@ describe("WalletFromSeed", () => { expect(result1.stakeKey).toBeUndefined() const result2 = yield* walletFromSeed(seedPhrase, { - addressType: "Enterprise" + addressType: "Enterprise", + networkId: 1 }) expect(Address.toBech32(result2.address)).toBe(Address.toBech32(result1.address)) expect(result2.rewardAddress).toBeUndefined() diff --git a/packages/evolution/test/provider/providers.test.ts b/packages/evolution/test/provider/providers.test.ts index 27a32f50..acdbb2cd 100644 --- a/packages/evolution/test/provider/providers.test.ts +++ b/packages/evolution/test/provider/providers.test.ts @@ -11,7 +11,7 @@ import { describe, expect, it } from "vitest" import { BlockfrostProvider } from "../../src/sdk/provider/Blockfrost.js" -import { Koios } from "../../src/sdk/provider/Koios.js" +import { KoiosProvider } from "../../src/sdk/provider/Koios.js" import { KupmiosProvider } from "../../src/sdk/provider/Kupmios.js" import { MaestroProvider } from "../../src/sdk/provider/Maestro.js" import { registerConformanceTests } from "./conformance.js" @@ -46,7 +46,7 @@ const OGMIOS_HEADER = parseHeaderJson(process.env.KUPMIOS_OGMIOS_HEADER_JSON) ?? // ── Koios (no API key) ──────────────────────────────────────────────────────── describe.skipIf(!process.env.KOIOS_ENABLED)("Koios", () => { - registerConformanceTests(() => new Koios(KOIOS_URL)) + registerConformanceTests(() => new KoiosProvider(KOIOS_URL)) }) // ── Koios preview: awaitTx with Haskell show string asset_list ──────────────── @@ -55,7 +55,7 @@ describe.skipIf(!process.env.KOIOS_ENABLED)("Koios", () => { // without the InputOutputSchema fix. describe.skipIf(!process.env.KOIOS_PREVIEW_ENABLED)("Koios (preview)", () => { it("awaitTx succeeds for tx with Haskell show string asset_list", { timeout: 200_000 }, async () => { - const koios = new Koios(KOIOS_PREVIEW_URL) + const koios = new KoiosProvider(KOIOS_PREVIEW_URL) const confirmed = await koios.awaitTx(previewTxHash()) expect(confirmed).toBe(true) }) diff --git a/packages/evolution/test/sdk/Client.test.ts b/packages/evolution/test/sdk/Client.test.ts new file mode 100644 index 00000000..94a88cfb --- /dev/null +++ b/packages/evolution/test/sdk/Client.test.ts @@ -0,0 +1,364 @@ +import { describe, expect, it } from "vitest" + +import * as Address from "../../src/Address.js" +import * as Assets from "../../src/Assets/index.js" +import type { + Addressable, + AwaitTx, + Client, + EvaluateTx, + QueryDatumByHash, + QueryDelegation, + QueryProtocolParams, + QueryUtxos, + QueryUtxosByOutRef, + Signable, + SubmitTx +} from "../../src/index.js" +import { + blockfrost, + client, + koios, + kupmios, + maestro, + mainnet, + newTx, + preprod, + preview, + readOnlyWallet +} from "../../src/index.js" +import type { TxBuilder } from "../../src/sdk/builders/TransactionBuilder.js" + +// ── Type-level tests ────────────────────────────────────────────────────────── + +// Verify client(chain) returns the right shape +const _baseClient = client(preview) +// @ts-expect-error — base client has no getUtxos +void _baseClient.getUtxos + +// Verify blockfrost adds capabilities +const _bfClient = client(preview) + .with(blockfrost({ baseUrl: "https://cardano-preview.blockfrost.io/api/v0", projectId: "test" })) +// These should type-check — blockfrost adds these capabilities +type _AssertBfHasGetUtxos = typeof _bfClient extends QueryUtxos ? true : never +type _AssertBfHasGetPP = typeof _bfClient extends QueryProtocolParams ? true : never +type _AssertBfHasSubmit = typeof _bfClient extends SubmitTx ? true : never +type _AssertBfHasOutRef = typeof _bfClient extends QueryUtxosByOutRef ? true : never +type _AssertBfHasDelegation = typeof _bfClient extends QueryDelegation ? true : never +type _AssertBfHasAwait = typeof _bfClient extends AwaitTx ? true : never +type _AssertBfHasDatum = typeof _bfClient extends QueryDatumByHash ? true : never +type _AssertBfHasEval = typeof _bfClient extends EvaluateTx ? true : never + +// Verify the client still carries chain context +type _AssertBfHasChain = typeof _bfClient extends Client ? true : never + +// Type assertion helper — forces TS to verify type assignment +const _assertType = (_v: T) => {} +_assertType(true as _AssertBfHasGetUtxos) +_assertType(true as _AssertBfHasGetPP) +_assertType(true as _AssertBfHasSubmit) +_assertType(true as _AssertBfHasChain) + +// Verify function constraints work +const _getBalance = async (c: QueryUtxos & Addressable): Promise => { + // This would work at runtime with real implementations + void c.getUtxos + void c.getAddress +} + +// Verify newTx return type narrows based on capabilities +const _newTxReadOnly = (c: Client & Addressable) => { + const tx = newTx(c) + // Type-level: Addressable without Signable → read-only TxBuilder + const _ro: TxBuilder = tx + void _ro +} +const _newTxSigning = (c: Client & Addressable & Signable) => { + const tx = newTx(c) + // Type-level: Addressable & Signable → signing TxBuilder + const _st: TxBuilder = tx + void _st +} + +const _signAndSubmit = async (c: SubmitTx & Signable, _tx: string): Promise => { + void c.submitTx + void c.signTx +} + +// ── Runtime tests ───────────────────────────────────────────────────────────── + +describe("Client API", () => { + describe("client()", () => { + it("creates a base client with chain context", () => { + const c = client(preview) + expect(c.chain).toBe(preview) + expect(c.networkId).toBe(0) + expect(c.Effect).toEqual({}) + }) + + it("carries correct networkId for mainnet", () => { + const c = client(mainnet) + expect(c.networkId).toBe(1) + }) + + it("carries correct networkId for preprod", () => { + const c = client(preprod) + expect(c.networkId).toBe(0) + }) + }) + + describe("blockfrost()", () => { + it("adds provider capabilities to client", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://cardano-preview.blockfrost.io/api/v0", projectId: "test" })) + + // Client context preserved + expect(c.chain).toBe(preview) + expect(c.networkId).toBe(0) + + // Provider methods exist + expect(typeof c.getUtxos).toBe("function") + expect(typeof c.getUtxosByOutRef).toBe("function") + expect(typeof c.getProtocolParameters).toBe("function") + expect(typeof c.getDelegation).toBe("function") + expect(typeof c.submitTx).toBe("function") + expect(typeof c.awaitTx).toBe("function") + expect(typeof c.getDatum).toBe("function") + expect(typeof c.evaluateTx).toBe("function") + + // Effect namespace exists + expect(typeof c.Effect.getUtxos).toBe("function") + expect(typeof c.Effect.submitTx).toBe("function") + }) + }) + + describe("maestro()", () => { + it("adds provider capabilities to client", () => { + const c = client(mainnet) + .with(maestro({ baseUrl: "https://mainnet.gomaestro-api.org/v1", apiKey: "test" })) + + expect(c.chain).toBe(mainnet) + expect(typeof c.getUtxos).toBe("function") + expect(typeof c.evaluateTx).toBe("function") + expect(typeof c.Effect.getUtxos).toBe("function") + }) + }) + + describe("koios()", () => { + it("adds provider capabilities to client", () => { + const c = client(preprod) + .with(koios({ baseUrl: "https://preprod.koios.rest/api/v1" })) + + expect(c.chain).toBe(preprod) + expect(typeof c.getUtxos).toBe("function") + expect(typeof c.getDelegation).toBe("function") + expect(typeof c.Effect.getUtxos).toBe("function") + }) + }) + + describe("kupmios()", () => { + it("adds provider capabilities to client", () => { + const c = client(preview) + .with(kupmios({ kupoUrl: "http://localhost:1442", ogmiosUrl: "ws://localhost:1337" })) + + expect(c.chain).toBe(preview) + expect(typeof c.getUtxos).toBe("function") + expect(typeof c.evaluateTx).toBe("function") + expect(typeof c.submitTx).toBe("function") + expect(typeof c.Effect.getUtxos).toBe("function") + }) + }) + + describe("composition", () => { + it("preserves chain context through provider middleware", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + + expect(c.chain.name).toBe("Cardano Preview") + expect(c.chain.networkMagic).toBe(2) + expect(c.chain.epochLength).toBe(86400) + }) + + it("Effect namespace merges across middleware", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + + // Effect namespace should have all provider Effect methods + expect(typeof c.Effect.getUtxos).toBe("function") + expect(typeof c.Effect.getProtocolParameters).toBe("function") + expect(typeof c.Effect.submitTx).toBe("function") + expect(typeof c.Effect.evaluateTx).toBe("function") + }) + + it("two providers — last wins for overlapping methods at runtime", () => { + const bfCfg = { baseUrl: "https://bf.test", projectId: "bf" } + const maestroCfg = { baseUrl: "https://maestro.test", apiKey: "maestro" } + + const c = client(preview) + .with(blockfrost(bfCfg)) + .with(maestro(maestroCfg)) + + // Both provider capabilities are present at the type level + expect(typeof c.getUtxos).toBe("function") + expect(typeof c.submitTx).toBe("function") + expect(typeof c.evaluateTx).toBe("function") + expect(typeof c.getDelegation).toBe("function") + + // Effect namespace has methods from both + expect(typeof c.Effect.getUtxos).toBe("function") + expect(typeof c.Effect.submitTx).toBe("function") + + // Chain context survives composition + expect(c.chain).toBe(preview) + expect(c.networkId).toBe(0) + }) + + it("provider + wallet — both capability sets present", () => { + // Type-level: seedWallet is tested for compilation only + // (runtime requires valid mnemonic + crypto libs) + const bfClient = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + + // Provider capabilities exist + expect(typeof bfClient.getUtxos).toBe("function") + expect(typeof bfClient.submitTx).toBe("function") + + // Effect namespace from provider + expect(typeof bfClient.Effect.getUtxos).toBe("function") + expect(typeof bfClient.Effect.submitTx).toBe("function") + }) + }) + + describe("newTx()", () => { + it("returns a transaction builder from provider-only client", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + + const tx = newTx(c) + + // Should be a ReadOnlyTransactionBuilder (no wallet) + expect(typeof tx.payToAddress).toBe("function") + expect(typeof tx.collectFrom).toBe("function") + expect(typeof tx.mintAssets).toBe("function") + expect(typeof tx.build).toBe("function") + + // Type-level: no signing wallet → read-only TxBuilder + const _tx: TxBuilder = tx + void _tx + }) + + it("returns a transaction builder from provider + readOnly wallet client", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + .with(readOnlyWallet("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae")) + + const tx = newTx(c) + + // Should still be ReadOnlyTransactionBuilder (no signTx) + expect(typeof tx.payToAddress).toBe("function") + expect(typeof tx.build).toBe("function") + + // Type-level: Addressable but not Signable → read-only TxBuilder + const _tx: TxBuilder = tx + void _tx + }) + + it("passes slotConfig from chain to builder", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + + // newTx should not throw — it just creates the builder + const tx = newTx(c) + expect(tx).toBeDefined() + }) + + it("creates builder without provider or wallet (manual mode)", () => { + const c = client(preview) + + const tx = newTx(c) + + // Should be a ReadOnlyTransactionBuilder with no provider/wallet + expect(typeof tx.payToAddress).toBe("function") + expect(typeof tx.build).toBe("function") + }) + + it("chains builder methods", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + + const tx = newTx(c) + .payToAddress({ + address: Address.fromBech32( + "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae" + ), + assets: Assets.fromLovelace(5_000_000n) + }) + + expect(typeof tx.build).toBe("function") + }) + + it("client.newTx() method returns the same shape as newTx(client)", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + + const fromMethod = c.newTx() + const fromFunction = newTx(c) + + expect(typeof fromMethod.payToAddress).toBe("function") + expect(typeof fromMethod.collectFrom).toBe("function") + expect(typeof fromMethod.build).toBe("function") + expect(typeof fromFunction.payToAddress).toBe("function") + expect(typeof fromFunction.build).toBe("function") + }) + + it("base client has newTx() method", () => { + const c = client(preview) + expect(typeof c.newTx).toBe("function") + const tx = c.newTx() + expect(typeof tx.payToAddress).toBe("function") + expect(typeof tx.build).toBe("function") + }) + + it("provider middleware preserves newTx() method", () => { + const c = client(preview) + .with(kupmios({ kupoUrl: "http://localhost:1442", ogmiosUrl: "ws://localhost:1337" })) + expect(typeof c.newTx).toBe("function") + const tx = c.newTx() + expect(typeof tx.collectFrom).toBe("function") + }) + }) + + describe(".with()", () => { + it("base client has .with() method", () => { + const c = client(preview) + expect(typeof c.with).toBe("function") + }) + + it("composes provider middleware chainably", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + + expect(c.chain).toBe(preview) + expect(typeof c.getUtxos).toBe("function") + expect(typeof c.with).toBe("function") + }) + + it("chains multiple middleware", () => { + const c = client(preview) + .with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + .with(readOnlyWallet("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae")) + + expect(typeof c.getUtxos).toBe("function") + expect(typeof c.getAddress).toBe("function") + expect(c.chain).toBe(preview) + }) + + it("preserves .with() after each middleware", () => { + const c1 = client(preview).with(blockfrost({ baseUrl: "https://test.com", projectId: "test" })) + expect(typeof c1.with).toBe("function") + + const c2 = c1.with(readOnlyWallet("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae")) + expect(typeof c2.with).toBe("function") + }) + }) +}) \ No newline at end of file