From 421cb9e66306ba14ad834e814dec2cbd25fe90b5 Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Thu, 7 Aug 2025 14:21:51 -0600 Subject: [PATCH 1/6] feat: upgrade module --- flake.nix | 2 +- package.json | 1 + packages/evolution/package.json | 15 +- packages/evolution/src/Address.ts | 205 ++++- packages/evolution/src/AddressDetails.ts | 185 +++- packages/evolution/src/Anchor.ts | 153 +++- packages/evolution/src/AssetName.ts | 153 +++- packages/evolution/src/AuxiliaryDataHash.ts | 157 +++- packages/evolution/src/BaseAddress.ts | 143 ++- packages/evolution/src/Bech32.ts | 9 +- packages/evolution/src/Bip32PrivateKey.ts | 744 ++++++++++++++++ packages/evolution/src/Bip32PublicKey.ts | 363 ++++++++ packages/evolution/src/Block.ts | 2 +- packages/evolution/src/BlockBodyHash.ts | 157 +++- packages/evolution/src/BlockHeaderHash.ts | 157 +++- packages/evolution/src/ByronAddress.ts | 57 +- packages/evolution/src/Bytes.ts | 10 - packages/evolution/src/Bytes128.ts | 123 +++ packages/evolution/src/Bytes16.ts | 153 +++- packages/evolution/src/Bytes29.ts | 135 ++- packages/evolution/src/Bytes32.ts | 167 +++- packages/evolution/src/Bytes4.ts | 151 +++- packages/evolution/src/Bytes448.ts | 135 ++- packages/evolution/src/Bytes57.ts | 135 ++- packages/evolution/src/Bytes64.ts | 160 +++- packages/evolution/src/Bytes80.ts | 135 ++- packages/evolution/src/Bytes96.ts | 123 +++ packages/evolution/src/CBOR.ts | 129 +-- packages/evolution/src/Certificate.ts | 610 ++++++++++++- packages/evolution/src/Coin.ts | 84 +- .../evolution/src/CommitteeColdCredential.ts | 45 +- .../evolution/src/CommitteeHotCredential.ts | 44 +- packages/evolution/src/Credential.ts | 147 +++- packages/evolution/src/DRep.ts | 364 +++++--- packages/evolution/src/DRepCredential.ts | 44 +- packages/evolution/src/Data.ts | 593 ++++++++++--- packages/evolution/src/DatumOption.ts | 154 +++- packages/evolution/src/DnsName.ts | 141 ++- packages/evolution/src/Ed25519Signature.ts | 157 +++- packages/evolution/src/EnterpriseAddress.ts | 150 +++- packages/evolution/src/GovernanceAction.ts | 829 ++++++++++++++++++ packages/evolution/src/Hash28.ts | 168 ++-- packages/evolution/src/Header.ts | 6 +- packages/evolution/src/HeaderBody.ts | 208 ++++- packages/evolution/src/IPv4.ts | 177 +++- packages/evolution/src/IPv6.ts | 149 +++- packages/evolution/src/KESVkey.ts | 149 +++- packages/evolution/src/KesSignature.ts | 149 +++- packages/evolution/src/KeyHash.ts | 267 +++++- packages/evolution/src/Mint.ts | 322 ++++--- packages/evolution/src/MultiAsset.ts | 231 ++++- packages/evolution/src/MultiHostName.ts | 139 ++- packages/evolution/src/NativeScripts.ts | 595 ++++++------- packages/evolution/src/Natural.ts | 141 ++- packages/evolution/src/Network.ts | 167 +++- packages/evolution/src/NetworkId.ts | 47 +- packages/evolution/src/NonZeroInt64.ts | 142 ++- packages/evolution/src/Numeric.ts | 120 ++- packages/evolution/src/OperationalCert.ts | 185 +++- packages/evolution/src/Pointer.ts | 28 +- packages/evolution/src/PointerAddress.ts | 156 +++- packages/evolution/src/PolicyId.ts | 157 +++- packages/evolution/src/PoolKeyHash.ts | 126 ++- packages/evolution/src/PoolMetadata.ts | 162 +++- packages/evolution/src/PoolParams.ts | 362 +++++--- packages/evolution/src/Port.ts | 4 +- packages/evolution/src/PositiveCoin.ts | 106 ++- packages/evolution/src/PrivateKey.ts | 423 +++++++++ packages/evolution/src/ProposalProcedures.ts | 359 +++++++- packages/evolution/src/ProtocolVersion.ts | 150 +++- packages/evolution/src/Relay.ts | 233 +++-- packages/evolution/src/RewardAccount.ts | 140 ++- packages/evolution/src/RewardAddress.ts | 48 +- packages/evolution/src/ScriptDataHash.ts | 202 ++++- packages/evolution/src/ScriptHash.ts | 130 ++- packages/evolution/src/ScriptRef.ts | 6 +- packages/evolution/src/SingleHostAddr.ts | 195 ++-- packages/evolution/src/SingleHostName.ts | 14 +- packages/evolution/src/TSchema.ts | 2 +- packages/evolution/src/Text.ts | 133 ++- packages/evolution/src/Text128.ts | 183 +++- packages/evolution/src/TransactionBody.ts | 381 +++++++- packages/evolution/src/TransactionHash.ts | 161 +++- packages/evolution/src/TransactionIndex.ts | 52 +- packages/evolution/src/TransactionInput.ts | 185 +++- packages/evolution/src/TransactionOutput.ts | 249 +++++- packages/evolution/src/UnitInterval.ts | 25 +- packages/evolution/src/Url.ts | 146 ++- packages/evolution/src/VKey.ts | 247 +++++- packages/evolution/src/Value.ts | 320 +++++-- packages/evolution/src/VotingProcedures.ts | 769 +++++++++++++++- packages/evolution/src/VrfCert.ts | 161 +++- packages/evolution/src/VrfKeyHash.ts | 118 ++- packages/evolution/src/VrfVkey.ts | 149 +++- packages/evolution/src/Withdrawals.ts | 251 ++++-- packages/evolution/src/index.ts | 6 + packages/evolution/test/Address.test.ts | 60 +- .../test/Bip32PrivateKey.CML.test.ts | 411 +++++++++ packages/evolution/test/Data.golden.test.ts | 108 ++- packages/evolution/test/Data.test.ts | 92 +- .../evolution/test/PrivateKey.CML.test.ts | 424 +++++++++ pnpm-lock.yaml | 179 +++- 102 files changed, 15822 insertions(+), 2774 deletions(-) create mode 100644 packages/evolution/src/Bip32PrivateKey.ts create mode 100644 packages/evolution/src/Bip32PublicKey.ts create mode 100644 packages/evolution/src/Bytes128.ts create mode 100644 packages/evolution/src/Bytes96.ts create mode 100644 packages/evolution/src/GovernanceAction.ts create mode 100644 packages/evolution/src/PrivateKey.ts create mode 100644 packages/evolution/test/Bip32PrivateKey.CML.test.ts create mode 100644 packages/evolution/test/PrivateKey.CML.test.ts diff --git a/flake.nix b/flake.nix index 01cab61c..fed34abc 100644 --- a/flake.nix +++ b/flake.nix @@ -21,7 +21,7 @@ devShells = forEachSupportedSystem ({ pkgs }: { default = pkgs.mkShell { - packages = with pkgs; [ nodejs nodePackages.pnpm ]; + packages = with pkgs; [ nodejs nodePackages.pnpm bun python3 ]; }; }); }; diff --git a/package.json b/package.json index 6be2cf0d..2c901001 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "madge": "^8.0.0", "prettier": "^3.5.0", "rimraf": "^6.0.0", + "tsx": "^4.20.3", "turbo": "^2.0.0", "typescript": "^5.8.0", "vitest": "^3.2.4" diff --git a/packages/evolution/package.json b/packages/evolution/package.json index c87ee113..19dec8db 100644 --- a/packages/evolution/package.json +++ b/packages/evolution/package.json @@ -37,16 +37,23 @@ "clean": "rm -rf dist .turbo docs" }, "devDependencies": { + "@dcspark/cardano-multiplatform-lib-nodejs": "^6.2.0", "@types/dockerode": "^3.3.0", + "@types/libsodium-wrappers-sumo": "^0.7.8", "tsx": "^4.20.3", "typescript": "^5.4.0" }, "dependencies": { - "@effect/platform": "^0.90.0", - "@effect/platform-node": "^0.94.0", - "@scure/base": "^1.1.0", + "@effect/platform-node": "^0.94.1", + "@noble/hashes": "^1.6.0", + "@scure/base": "^1.2.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "@types/bip39": "^3.0.4", + "bip39": "^3.1.0", "dockerode": "^4.0.0", - "effect": "^3.17.3" + "effect": "^3.17.3", + "libsodium-wrappers-sumo": "^0.7.15" }, "keywords": [ "cardano", diff --git a/packages/evolution/src/Address.ts b/packages/evolution/src/Address.ts index 04e6578e..692398ab 100644 --- a/packages/evolution/src/Address.ts +++ b/packages/evolution/src/Address.ts @@ -1,10 +1,9 @@ -import { Data, Effect, FastCheck, ParseResult, pipe, Schema } from "effect" +import { bech32 } from "@scure/base" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as BaseAddress from "./BaseAddress.js" -import * as Bech32 from "./Bech32.js" import * as ByronAddress from "./ByronAddress.js" import * as Bytes from "./Bytes.js" -import { createEncoders } from "./Codec.js" import * as EnterpriseAddress from "./EnterpriseAddress.js" import * as PointerAddress from "./PointerAddress.js" import * as RewardAccount from "./RewardAccount.js" @@ -99,7 +98,7 @@ export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, Addre } }, decode: (_, __, ast, fromA) => - Effect.gen(function* () { + Eff.gen(function* () { const header = fromA[0] // Extract address type from the upper 4 bits (bits 4-7) const addressType = header >> 4 @@ -152,10 +151,10 @@ export const FromHex = Schema.compose(Bytes.FromHex, FromBytes) * @since 2.0.0 * @category schema */ -export const FromBech32 = Schema.transformOrFail(Schema.typeSchema(Bech32.Bech32Schema), Address, { +export const FromBech32 = Schema.transformOrFail(Schema.String, Address, { strict: true, - encode: (_, __, ___, toA) => - Effect.gen(function* () { + encode: (_, __, ast, toA) => + Eff.gen(function* () { const bytes = yield* ParseResult.encode(FromBytes)(toA) let prefix: string switch (toA._tag) { @@ -168,13 +167,34 @@ export const FromBech32 = Schema.transformOrFail(Schema.typeSchema(Bech32.Bech32 prefix = toA.networkId === 0 ? "stake_test" : "stake" break case "ByronAddress": - prefix = "" - break + return yield* ParseResult.fail( + new ParseResult.Type(ast, toA, "Byron addresses do not support Bech32 encoding") + ) } - const b = yield* ParseResult.decode(Bech32.FromBytes(prefix))(bytes) - return b + const result = yield* Eff.try({ + try: () => { + const words = bech32.toWords(bytes) + return bech32.encode(prefix, words, false) + }, + catch: (error) => new ParseResult.Type(ast, toA, `Failed to encode Bech32: ${(error as Error).message}`) + }) + return result }), - decode: (fromI) => pipe(ParseResult.encode(Bech32.FromBytes())(fromI), Effect.flatMap(ParseResult.decode(FromBytes))) + decode: (fromA, _, ast) => + Eff.gen(function* () { + const result = yield* Eff.try({ + try: () => { + const decoded = bech32.decode(fromA as any, false) + const bytes = bech32.fromWords(decoded.words) + return new Uint8Array(bytes) + }, + catch: (error) => new ParseResult.Type(ast, fromA, `Failed to decode Bech32: ${(error as Error).message}`) + }) + return yield* ParseResult.decode(FromBytes)(result) + }) +}).annotations({ + identifier: "Address.FromBech32", + description: "Transforms Bech32 string to Address" }) /** @@ -202,29 +222,156 @@ export const equals = (a: Address, b: Address): boolean => { } /** - * FastCheck generator for addresses. + * FastCheck arbitrary for Address instances. * * @since 2.0.0 - * @category testing + * @category arbitrary + * */ -export const generator = FastCheck.oneof( - BaseAddress.generator, - EnterpriseAddress.generator, - PointerAddress.generator, - RewardAccount.generator +export const arbitrary = FastCheck.oneof( + BaseAddress.arbitrary, + EnterpriseAddress.arbitrary, + PointerAddress.arbitrary, + RewardAccount.arbitrary ) +// ============================================================================ +// Parsing Functions +// ============================================================================ + /** - * Codec utilities for addresses. + * Parse an Address from bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bech32: FromBech32, - hex: FromHex, - bytes: FromBytes - }, - AddressError -) +export const fromBytes = (bytes: Uint8Array): Address => Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse an Address from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): Address => Eff.runSync(Effect.fromHex(hex)) + +/** + * Parse an Address from Bech32 string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBech32 = (bech32: string): Address => Eff.runSync(Effect.fromBech32(bech32)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert an Address to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (address: Address): Uint8Array => Eff.runSync(Effect.toBytes(address)) + +/** + * Convert an Address to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (address: Address): string => Eff.runSync(Effect.toHex(address)) + +/** + * Convert an Address to Bech32 string. + * + * @since 2.0.0 + * @category encoding + */ +export const toBech32 = (address: Address): string => Eff.runSync(Effect.toBech32(address)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse an Address from bytes. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array) => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (error) => new AddressError({ message: "Failed to decode Address from bytes", cause: error }) + ) + + /** + * Parse an Address from hex string. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string) => + Eff.mapError( + Schema.decode(FromHex)(hex), + (error) => new AddressError({ message: "Failed to decode Address from hex", cause: error }) + ) + + /** + * Parse an Address from Bech32 string. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBech32 = (bech32: string) => + Eff.mapError( + Schema.decode(FromBech32)(bech32), + (error) => new AddressError({ message: "Failed to decode Address from Bech32", cause: error }) + ) + + /** + * Convert an Address to bytes. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (address: Address) => + Eff.mapError( + Schema.encode(FromBytes)(address), + (error) => new AddressError({ message: "Failed to encode Address to bytes", cause: error }) + ) + + /** + * Convert an Address to hex string. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (address: Address) => + Eff.mapError( + Schema.encode(FromHex)(address), + (error) => new AddressError({ message: "Failed to encode Address to hex", cause: error }) + ) + + /** + * Convert an Address to Bech32 string. + * + * @since 2.0.0 + * @category encoding + */ + export const toBech32 = (address: Address) => + Eff.mapError( + Schema.encode(FromBech32)(address), + (error) => new AddressError({ message: "Failed to encode Address to Bech32", cause: error }) + ) +} diff --git a/packages/evolution/src/AddressDetails.ts b/packages/evolution/src/AddressDetails.ts index a9b40e73..1276ed2d 100644 --- a/packages/evolution/src/AddressDetails.ts +++ b/packages/evolution/src/AddressDetails.ts @@ -1,9 +1,7 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, ParseResult, Schema } from "effect" import * as Address from "./Address.js" -import * as _Bech32 from "./Bech32.js" import * as Bytes from "./Bytes.js" -import * as _Codec from "./Codec.js" import * as NetworkId from "./NetworkId.js" export class AddressDetailsError extends Data.TaggedError("AddressDetailsError")<{ @@ -12,20 +10,13 @@ export class AddressDetailsError extends Data.TaggedError("AddressDetailsError") }> {} /** - * Extended address information with both structured data and serialized formats + * Schema for AddressDetails representing extended address information. * Contains the address structure and its serialized representations * * @since 2.0.0 - * @category model - */ - -/** - * Pointer address with payment credential and pointer to stake registration - * - * @since 2.0.0 * @category schemas */ -export class AddressDetails extends Schema.TaggedClass("AddressDetails")("AddressDetails", { +export class AddressDetails extends Schema.Class("AddressDetails")({ networkId: NetworkId.NetworkId, type: Schema.Union( Schema.Literal("BaseAddress"), @@ -35,15 +26,15 @@ export class AddressDetails extends Schema.TaggedClass("AddressD Schema.Literal("ByronAddress") ), address: Address.Address, - bech32: _Bech32.Bech32Schema, + bech32: Schema.String, hex: Bytes.HexSchema }) {} -export const FromBech32 = Schema.transformOrFail(Schema.typeSchema(_Bech32.Bech32Schema), AddressDetails, { +export const FromBech32 = Schema.transformOrFail(Schema.String, AddressDetails, { strict: true, encode: (_, __, ___, toA) => ParseResult.succeed(toA.bech32), decode: (_, __, ___, fromA) => - Effect.gen(function* () { + Eff.gen(function* () { const address = yield* ParseResult.decode(Address.FromBech32)(fromA) const hex = yield* ParseResult.encode(Address.FromHex)(address) return new AddressDetails({ @@ -60,7 +51,7 @@ export const FromHex = Schema.transformOrFail(Bytes.HexSchema, AddressDetails, { strict: true, encode: (_, __, ___, toA) => ParseResult.succeed(toA.hex), decode: (_, __, ___, fromA) => - Effect.gen(function* () { + Eff.gen(function* () { const address = yield* ParseResult.decode(Address.FromHex)(fromA) const bech32 = yield* ParseResult.encode(Address.FromBech32)(address) return new AddressDetails({ @@ -73,10 +64,158 @@ export const FromHex = Schema.transformOrFail(Bytes.HexSchema, AddressDetails, { }) }) -export const Codec = _Codec.createEncoders( - { - bech32: FromBech32, - hex: FromHex - }, - AddressDetailsError -) +/** + * Create AddressDetails from an Address instance. + * + * @since 2.0.0 + * @category constructors + */ +export const make = AddressDetails.make + +/** + * Check if two AddressDetails instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (self: AddressDetails, that: AddressDetails): boolean => { + return ( + self.networkId === that.networkId && + self.type === that.type && + Address.equals(self.address, that.address) && + self.bech32 === that.bech32 && + self.hex === that.hex + ) +} + +/** + * FastCheck arbitrary for AddressDetails instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = Address.arbitrary.map((address) => fromAddress(address)) + +/** + * Create AddressDetails from an Address. + * + * @since 2.0.0 + * @category constructors + */ +export const fromAddress = (address: Address.Address): AddressDetails => { + // Use schema encoding to get the serialized formats + const bech32 = Eff.runSync(Schema.encode(Address.FromBech32)(address)) + const hex = Eff.runSync(Schema.encode(Address.FromHex)(address)) + return new AddressDetails({ + networkId: address.networkId, + type: address._tag, + address, + bech32, + hex + }) +} + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse AddressDetails from Bech32 string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBech32 = (bech32: string): AddressDetails => + Eff.runSync(Effect.fromBech32(bech32)) + +/** + * Parse AddressDetails from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): AddressDetails => + Eff.runSync(Effect.fromHex(hex)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert AddressDetails to Bech32 string. + * + * @since 2.0.0 + * @category encoding + */ +export const toBech32 = (details: AddressDetails): string => + Eff.runSync(Effect.toBech32(details)) + +/** + * Convert AddressDetails to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (details: AddressDetails): string => + Eff.runSync(Effect.toHex(details)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse AddressDetails from Bech32 string. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBech32 = (bech32: string) => + Eff.mapError( + Schema.decode(FromBech32)(bech32), + (error) => new AddressDetailsError({ message: "Failed to decode AddressDetails from Bech32", cause: error }) + ) + + /** + * Parse AddressDetails from hex string. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string) => + Eff.mapError( + Schema.decode(FromHex)(hex), + (error) => new AddressDetailsError({ message: "Failed to decode AddressDetails from hex", cause: error }) + ) + + /** + * Convert AddressDetails to Bech32 string. + * + * @since 2.0.0 + * @category encoding + */ + export const toBech32 = (details: AddressDetails) => + Eff.mapError( + Schema.encode(FromBech32)(details), + (error) => new AddressDetailsError({ message: "Failed to encode AddressDetails to Bech32", cause: error }) + ) + + /** + * Convert AddressDetails to hex string. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (details: AddressDetails) => + Eff.mapError( + Schema.encode(FromHex)(details), + (error) => new AddressDetailsError({ message: "Failed to encode AddressDetails to hex", cause: error }) + ) +} diff --git a/packages/evolution/src/Anchor.ts b/packages/evolution/src/Anchor.ts index 69819865..205022ec 100644 --- a/packages/evolution/src/Anchor.ts +++ b/packages/evolution/src/Anchor.ts @@ -1,9 +1,8 @@ -import { Data, Effect, FastCheck, ParseResult, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, pipe, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as Bytes32 from "./Bytes32.js" import * as CBOR from "./CBOR.js" -import { createEncoders } from "./Codec.js" import * as Url from "./Url.js" /** @@ -14,7 +13,7 @@ import * as Url from "./Url.js" */ export class AnchorError extends Data.TaggedError("AnchorError")<{ message?: string - reason?: "InvalidStructure" | "InvalidUrl" | "InvalidHash" + cause?: unknown }> {} /** @@ -22,13 +21,18 @@ export class AnchorError extends Data.TaggedError("AnchorError")<{ * anchor = [anchor_url: url, anchor_data_hash: Bytes32] * * @since 2.0.0 - * @category model + * @category schemas */ -export class Anchor extends Schema.TaggedClass()("Anchor", { +export class Anchor extends Schema.Class("Anchor")({ anchorUrl: Url.Url, anchorDataHash: Bytes32.HexSchema }) {} +export const CDDLSchema = Schema.Tuple( + CBOR.Text, // anchor_url: url + CBOR.ByteArray // anchor_data_hash: Bytes32 +) + /** * CDDL schema for Anchor as tuple structure. * anchor = [anchor_url: url, anchor_data_hash: Bytes32] @@ -36,17 +40,17 @@ export class Anchor extends Schema.TaggedClass()("Anchor", { * @since 2.0.0 * @category schemas */ -export const FromCDDL = Schema.transformOrFail(Schema.Tuple(CBOR.Text, CBOR.ByteArray), Schema.typeSchema(Anchor), { +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Anchor), { strict: true, encode: (toA) => pipe( ParseResult.encode(Bytes32.FromBytes)(toA.anchorDataHash), - Effect.map((anchorDataHash) => [toA.anchorUrl, anchorDataHash] as const) + Eff.map((anchorDataHash) => [toA.anchorUrl, anchorDataHash] as const) ), decode: ([anchorUrl, anchorDataHash]) => pipe( ParseResult.decode(Bytes32.FromBytes)(anchorDataHash), - Effect.map( + Eff.map( (anchorDataHash) => new Anchor({ anchorUrl: Url.make(anchorUrl), @@ -62,7 +66,7 @@ export const FromCDDL = Schema.transformOrFail(Schema.Tuple(CBOR.Text, CBOR.Byte * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → Anchor @@ -74,10 +78,10 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → Anchor + FromCBORBytes(options) // Uint8Array → Anchor ) /** @@ -108,27 +112,126 @@ export const equals = (self: Anchor, that: Anchor): boolean => { } /** - * FastCheck generator for Anchor instances. + * FastCheck arbitrary for Anchor instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.record({ - anchorUrl: Url.generator, - anchorDataHash: FastCheck.uint8Array({ minLength: 32, maxLength: 32 }) +export const arbitrary = FastCheck.record({ + anchorUrl: Url.arbitrary, + anchorDataHash: FastCheck.hexaString({ + minLength: Bytes32.HEX_LENGTH, + maxLength: Bytes32.HEX_LENGTH + }) }).map( ({ anchorDataHash, anchorUrl }) => new Anchor({ anchorUrl, - anchorDataHash: Bytes.Codec.Decode.bytes(anchorDataHash) + anchorDataHash }) ) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - AnchorError - ) +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse an Anchor from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Anchor => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse an Anchor from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Anchor => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert an Anchor to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (value: Anchor, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(value, options)) + +/** + * Convert an Anchor to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (value: Anchor, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(value, options)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse an Anchor from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (error) => new AnchorError({ message: "Failed to decode Anchor from CBOR bytes", cause: error }) + ) + + /** + * Parse an Anchor from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (error) => new AnchorError({ message: "Failed to decode Anchor from CBOR hex", cause: error }) + ) + + /** + * Convert an Anchor to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (value: Anchor, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(value), + (error) => new AnchorError({ message: "Failed to encode Anchor to CBOR bytes", cause: error }) + ) + + /** + * Convert an Anchor to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (value: Anchor, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(value), + (error) => new AnchorError({ message: "Failed to encode Anchor to CBOR hex", cause: error }) + ) +} diff --git a/packages/evolution/src/AssetName.ts b/packages/evolution/src/AssetName.ts index 0a67ffbb..057866c3 100644 --- a/packages/evolution/src/AssetName.ts +++ b/packages/evolution/src/AssetName.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" /** * Error class for AssetName related operations. @@ -47,6 +46,14 @@ export const FromHex = Schema.compose(Bytes32.VariableHexSchema, AssetName).anno identifier: "AssetName.Hex" }) +/** + * Smart constructor for AssetName that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = AssetName.make + /** * Check if two AssetName instances are equal. * @@ -56,26 +63,140 @@ export const FromHex = Schema.compose(Bytes32.VariableHexSchema, AssetName).anno export const equals = (a: AssetName, b: AssetName): boolean => a === b /** - * Generate a random AssetName. + * Check if the given value is a valid AssetName + * + * @since 2.0.0 + * @category predicates + */ +export const isAssetName = Schema.is(AssetName) + +/** + * FastCheck arbitrary for generating random AssetName instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ +export const arbitrary = FastCheck.hexaString({ minLength: 0, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) + maxLength: Bytes32.HEX_LENGTH +}).map((hex) => fromHex(hex)) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for AssetName encoding and decoding operations. + * Parse AssetName from bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - AssetNameError -) +export const fromBytes = (bytes: Uint8Array): AssetName => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse AssetName from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): AssetName => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode AssetName to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (assetName: AssetName): Uint8Array => + Eff.runSync(Effect.toBytes(assetName)) + +/** + * Encode AssetName to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (assetName: AssetName): string => + Eff.runSync(Effect.toHex(assetName)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse AssetName from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new AssetNameError({ + message: "Failed to parse AssetName from bytes", + cause + }) + ) + ) + + /** + * Parse AssetName from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new AssetNameError({ + message: "Failed to parse AssetName from hex", + cause + }) + ) + ) + + /** + * Encode AssetName to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (assetName: AssetName): Eff.Effect => + Schema.encode(FromBytes)(assetName).pipe( + Eff.mapError( + (cause) => + new AssetNameError({ + message: "Failed to encode AssetName to bytes", + cause + }) + ) + ) + + /** + * Encode AssetName to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (assetName: AssetName): Eff.Effect => + Schema.encode(FromHex)(assetName).pipe( + Eff.mapError( + (cause) => + new AssetNameError({ + message: "Failed to encode AssetName to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/AuxiliaryDataHash.ts b/packages/evolution/src/AuxiliaryDataHash.ts index cbadd341..ecebe05b 100644 --- a/packages/evolution/src/AuxiliaryDataHash.ts +++ b/packages/evolution/src/AuxiliaryDataHash.ts @@ -7,10 +7,9 @@ * @since 2.0.0 */ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" /** * Error class for AuxiliaryDataHash related operations. @@ -30,7 +29,7 @@ export class AuxiliaryDataHashError extends Data.TaggedError("AuxiliaryDataHashE * @since 2.0.0 * @category schemas */ -export const AuxiliaryDataHash = pipe(Bytes32.HexSchema, Schema.brand("AuxiliaryDataHash")).annotations({ +export const AuxiliaryDataHash = Bytes32.HexSchema.pipe(Schema.brand("AuxiliaryDataHash")).annotations({ identifier: "AuxiliaryDataHash" }) @@ -50,6 +49,14 @@ export const HexSchema = Schema.compose( identifier: "AuxiliaryDataHash.Hex" }) +/** + * Smart constructor for AuxiliaryDataHash that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = AuxiliaryDataHash.make + /** * Check if two AuxiliaryDataHash instances are equal. * @@ -59,26 +66,140 @@ export const HexSchema = Schema.compose( export const equals = (a: AuxiliaryDataHash, b: AuxiliaryDataHash): boolean => a === b /** - * Generate a random AuxiliaryDataHash. + * Check if the given value is a valid AuxiliaryDataHash + * + * @since 2.0.0 + * @category predicates + */ +export const isAuxiliaryDataHash = Schema.is(AuxiliaryDataHash) + +/** + * FastCheck arbitrary for generating random AuxiliaryDataHash instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes32.Bytes32_BYTES_LENGTH, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes32.HEX_LENGTH, + maxLength: Bytes32.HEX_LENGTH +}).map((hex) => hex as AuxiliaryDataHash) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for AuxiliaryDataHash encoding and decoding operations. + * Parse AuxiliaryDataHash from bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: BytesSchema, - hex: HexSchema - }, - AuxiliaryDataHashError -) +export const fromBytes = (bytes: Uint8Array): AuxiliaryDataHash => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse AuxiliaryDataHash from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): AuxiliaryDataHash => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode AuxiliaryDataHash to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (auxDataHash: AuxiliaryDataHash): Uint8Array => + Eff.runSync(Effect.toBytes(auxDataHash)) + +/** + * Encode AuxiliaryDataHash to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (auxDataHash: AuxiliaryDataHash): string => + Eff.runSync(Effect.toHex(auxDataHash)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse AuxiliaryDataHash from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(BytesSchema)(bytes).pipe( + Eff.mapError( + (cause) => + new AuxiliaryDataHashError({ + message: "Failed to parse AuxiliaryDataHash from bytes", + cause + }) + ) + ) + + /** + * Parse AuxiliaryDataHash from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(HexSchema)(hex).pipe( + Eff.mapError( + (cause) => + new AuxiliaryDataHashError({ + message: "Failed to parse AuxiliaryDataHash from hex", + cause + }) + ) + ) + + /** + * Encode AuxiliaryDataHash to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (auxDataHash: AuxiliaryDataHash): Eff.Effect => + Schema.encode(BytesSchema)(auxDataHash).pipe( + Eff.mapError( + (cause) => + new AuxiliaryDataHashError({ + message: "Failed to encode AuxiliaryDataHash to bytes", + cause + }) + ) + ) + + /** + * Encode AuxiliaryDataHash to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (auxDataHash: AuxiliaryDataHash): Eff.Effect => + Schema.encode(HexSchema)(auxDataHash).pipe( + Eff.mapError( + (cause) => + new AuxiliaryDataHashError({ + message: "Failed to encode AuxiliaryDataHash to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/BaseAddress.ts b/packages/evolution/src/BaseAddress.ts index af8576bc..26af8efc 100644 --- a/packages/evolution/src/BaseAddress.ts +++ b/packages/evolution/src/BaseAddress.ts @@ -1,8 +1,7 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as Bytes57 from "./Bytes57.js" -import * as _Codec from "./Codec.js" import * as Credential from "./Credential.js" import * as KeyHash from "./KeyHash.js" import * as NetworkId from "./NetworkId.js" @@ -23,21 +22,12 @@ export class BaseAddress extends Schema.TaggedClass("BaseAddress")( networkId: NetworkId.NetworkId, paymentCredential: Credential.Credential, stakeCredential: Credential.Credential -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: "BaseAddress", - networkId: this.networkId, - paymentCredential: this.paymentCredential, - stakeCredential: this.stakeCredential - } - } -} +}) {} export const FromBytes = Schema.transformOrFail(Bytes57.BytesSchema, BaseAddress, { strict: true, encode: (_, __, ___, toA) => - Effect.gen(function* () { + Eff.gen(function* () { const paymentBit = toA.paymentCredential._tag === "KeyHash" ? 0 : 1 const stakeBit = toA.stakeCredential._tag === "KeyHash" ? 0 : 1 const header = (0b00 << 6) | (stakeBit << 5) | (paymentBit << 4) | (toA.networkId & 0b00001111) @@ -53,7 +43,7 @@ export const FromBytes = Schema.transformOrFail(Bytes57.BytesSchema, BaseAddress return yield* ParseResult.succeed(result) }), decode: (fromI, options, ast, fromA) => - Effect.gen(function* () { + Eff.gen(function* () { const header = fromA[0] // Extract network ID from the lower 4 bits const networkId = header & 0b00001111 @@ -68,7 +58,7 @@ export const FromBytes = Schema.transformOrFail(Bytes57.BytesSchema, BaseAddress } : { _tag: "ScriptHash", - hash: yield* ParseResult.decode(ScriptHash.BytesSchema)(fromA.slice(1, 29)) + hash: yield* ParseResult.decode(ScriptHash.FromBytes)(fromA.slice(1, 29)) } const isStakeKey = (addressType & 0b0010) === 0 const stakeCredential: Credential.Credential = isStakeKey @@ -78,7 +68,7 @@ export const FromBytes = Schema.transformOrFail(Bytes57.BytesSchema, BaseAddress } : { _tag: "ScriptHash", - hash: yield* ParseResult.decode(ScriptHash.BytesSchema)(fromA.slice(29, 57)) + hash: yield* ParseResult.decode(ScriptHash.FromBytes)(fromA.slice(29, 57)) } return yield* ParseResult.decode(BaseAddress)({ _tag: "BaseAddress", @@ -110,12 +100,20 @@ export const equals = (a: BaseAddress, b: BaseAddress): boolean => { } /** - * Generate a random BaseAddress. + * Smart constructor for BaseAddress. + * + * @since 2.0.0 + * @category constructors + */ +export const make = Schema.decodeSync(BaseAddress) + +/** + * FastCheck arbitrary for BaseAddress instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.tuple(NetworkId.generator, Credential.generator, Credential.generator).map( +export const arbitrary = FastCheck.tuple(NetworkId.arbitrary, Credential.arbitrary, Credential.arbitrary).map( ([networkId, paymentCredential, stakeCredential]) => new BaseAddress({ networkId, @@ -124,10 +122,103 @@ export const generator = FastCheck.tuple(NetworkId.generator, Credential.generat }) ) -export const Codec = _Codec.createEncoders( - { - hex: FromHex, - bytes: FromBytes - }, - BaseAddressError -) +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse a BaseAddress from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): BaseAddress => Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse a BaseAddress from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): BaseAddress => Eff.runSync(Effect.fromHex(hex)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert a BaseAddress to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (address: BaseAddress): Uint8Array => Eff.runSync(Effect.toBytes(address)) + +/** + * Convert a BaseAddress to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (address: BaseAddress): string => Eff.runSync(Effect.toHex(address)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse a BaseAddress from bytes. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array) => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (error) => new BaseAddressError({ message: "Failed to decode BaseAddress from bytes", cause: error }) + ) + + /** + * Parse a BaseAddress from hex string. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string) => + Eff.mapError( + Schema.decode(FromHex)(hex), + (error) => new BaseAddressError({ message: "Failed to decode BaseAddress from hex", cause: error }) + ) + + /** + * Convert a BaseAddress to bytes. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (address: BaseAddress) => + Eff.mapError( + Schema.encode(FromBytes)(address), + (error) => new BaseAddressError({ message: "Failed to encode BaseAddress to bytes", cause: error }) + ) + + /** + * Convert a BaseAddress to hex string. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (address: BaseAddress) => + Eff.mapError( + Schema.encode(FromHex)(address), + (error) => new BaseAddressError({ message: "Failed to encode BaseAddress to hex", cause: error }) + ) +} diff --git a/packages/evolution/src/Bech32.ts b/packages/evolution/src/Bech32.ts index 0d8553e4..4233e982 100644 --- a/packages/evolution/src/Bech32.ts +++ b/packages/evolution/src/Bech32.ts @@ -1,6 +1,8 @@ import { bech32 } from "@scure/base" import { Data, Effect, ParseResult, Schema } from "effect" +import * as Bytes from "./Bytes.js" + /** * @since 2.0.0 * @category model @@ -21,8 +23,13 @@ export const FromBytes = (prefix: string = "addr") => try: () => bech32.decodeToBytes(toA).bytes, catch: () => new ParseResult.Type(ast, toA, ` ${toA} is not a valid Bech32 address`) }), - decode: (fromA, options, ast, fromI) => { + decode: (_, __, ___, fromI) => { const words = bech32.toWords(fromI) return ParseResult.succeed(bech32.encode(prefix, words, false)) } }) + +export const FromHex = (prefix: string = "addr") => + Schema.compose(Bytes.FromHex, FromBytes(prefix)).annotations({ + identifier: "Bech32.FromHex" + }) diff --git a/packages/evolution/src/Bip32PrivateKey.ts b/packages/evolution/src/Bip32PrivateKey.ts new file mode 100644 index 00000000..afb51a9d --- /dev/null +++ b/packages/evolution/src/Bip32PrivateKey.ts @@ -0,0 +1,744 @@ +import { pbkdf2 } from "@noble/hashes/pbkdf2" +import { sha512 } from "@noble/hashes/sha2" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" +import sodium from "libsodium-wrappers-sumo" + +import * as Bip32PublicKey from "./Bip32PublicKey.js" +import * as Bytes96 from "./Bytes96.js" +import * as PrivateKey from "./PrivateKey.js" + +// Initialize libsodium +await sodium.ready + +/** + * Error class for Bip32PrivateKey related operations. + * + * @since 2.0.0 + * @category errors + */ +export class Bip32PrivateKeyError extends Data.TaggedError("Bip32PrivateKeyError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Schema for Bip32PrivateKey representing a BIP32-Ed25519 extended private key. + * Always 96 bytes: 32-byte scalar + 32-byte IV + 32-byte chaincode. + * Follows BIP32-Ed25519 hierarchical deterministic key derivation. + * Uses V2 derivation scheme for full CML (Cardano Multiplatform Library) compatibility. + * + * @since 2.0.0 + * @category schemas + */ +export const Bip32PrivateKey = Bytes96.HexSchema + .pipe(Schema.brand("Bip32PrivateKey")) + .annotations({ + identifier: "Bip32PrivateKey" + }) + +export type Bip32PrivateKey = typeof Bip32PrivateKey.Type + +export const FromBytes = Schema.compose( + Bytes96.FromBytes, // Uint8Array -> hex string + Bip32PrivateKey // hex string -> Bip32PrivateKey +).annotations({ + identifier: "Bip32PrivateKey.Bytes" +}) + +export const FromHex = Schema.compose( + Bytes96.HexSchema, // string -> hex string + Bip32PrivateKey // hex string -> Bip32PrivateKey +).annotations({ + identifier: "Bip32PrivateKey.Hex" +}) + +// Constants for BIP32-Ed25519 +const SCALAR_INDEX = 0 +const SCALAR_SIZE = 32 +const CHAIN_CODE_INDEX = 64 +const CHAIN_CODE_SIZE = 32 +const PBKDF2_ITERATIONS = 4096 +const PBKDF2_KEY_SIZE = 96 + +/** + * Clamp the scalar by: + * 1. Clearing the 3 lower bits + * 2. Clearing the three highest bits + * 3. Setting the second-highest bit + * + * This follows Ed25519-BIP32 specification requirements. + */ +const clampScalar = (scalar: Uint8Array): Uint8Array => { + const clamped = new Uint8Array(scalar) + clamped[0] &= 0b1111_1000 + clamped[31] &= 0b0001_1111 + clamped[31] |= 0b0100_0000 + return clamped +} + +/** + * Extract the scalar part (first 32 bytes) from the extended key. + */ +const extractScalar = (extendedKey: Uint8Array): Uint8Array => + extendedKey.slice(SCALAR_INDEX, SCALAR_SIZE) + +/** + * Extract the chaincode part (bytes 64-95) from the extended key. + */ +const extractChainCode = (extendedKey: Uint8Array): Uint8Array => + extendedKey.slice(CHAIN_CODE_INDEX, CHAIN_CODE_INDEX + CHAIN_CODE_SIZE) + +/** + * Smart constructor for Bip32PrivateKey that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = Bip32PrivateKey.make + +/** + * Check if two Bip32PrivateKey instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: Bip32PrivateKey, b: Bip32PrivateKey): boolean => a === b + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse a Bip32PrivateKey from raw bytes (96 bytes). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): Bip32PrivateKey => Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse a Bip32PrivateKey from a hex string (192 hex characters). + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): Bip32PrivateKey => Eff.runSync(Effect.fromHex(hex)) + +/** + * Create a Bip32PrivateKey from BIP39 entropy with PBKDF2 key stretching. + * This is the proper way to generate a master key from a BIP39 seed. + * + * @since 2.0.0 + * @category bip39 + */ +export const fromBip39Entropy = (entropy: Uint8Array, password: string = ""): Bip32PrivateKey => + Eff.runSync(Effect.fromBip39Entropy(entropy, password)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert a Bip32PrivateKey to raw bytes (96 bytes). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (bip32PrivateKey: Bip32PrivateKey): Uint8Array => + Eff.runSync(Effect.toBytes(bip32PrivateKey)) + +/** + * Convert a Bip32PrivateKey to a hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (bip32PrivateKey: Bip32PrivateKey): string => bip32PrivateKey // Already a hex string + +// ============================================================================ +// Key Derivation +// ============================================================================ + +/** + * Derive a child private key using a single derivation index. + * Supports both hard derivation (>= 0x80000000) and soft derivation (< 0x80000000). + * + * @since 2.0.0 + * @category bip32 + */ +export const deriveChild = (bip32PrivateKey: Bip32PrivateKey, index: number): Bip32PrivateKey => + Eff.runSync(Effect.deriveChild(bip32PrivateKey, index)) + +/** + * Derive a child private key using multiple derivation indices. + * Each index can be either hard or soft derivation. + * + * @since 2.0.0 + * @category bip32 + */ +export const derive = (bip32PrivateKey: Bip32PrivateKey, indices: Array): Bip32PrivateKey => + Eff.runSync(Effect.derive(bip32PrivateKey, indices)) + +/** + * Derive a child private key using a BIP32 path string. + * Supports paths like "m/1852'/1815'/0'/0/0" or "1852'/1815'/0'/0/0". + * + * @since 2.0.0 + * @category bip32 + */ +export const derivePath = (bip32PrivateKey: Bip32PrivateKey, path: string): Bip32PrivateKey => + Eff.runSync(Effect.derivePath(bip32PrivateKey, path)) + +// ============================================================================ +// Key Conversion +// ============================================================================ + +/** + * Convert a Bip32PrivateKey to a standard PrivateKey for signing operations. + * Extracts the first 64 bytes (scalar + IV) for Ed25519 signing. + * + * @since 2.0.0 + * @category conversion + */ +export const toPrivateKey = (bip32PrivateKey: Bip32PrivateKey): PrivateKey.PrivateKey => + Eff.runSync(Effect.toPrivateKey(bip32PrivateKey)) + +// ============================================================================ +// CML Compatibility Functions +// ============================================================================ + +/** + * Serialize Bip32PrivateKey to CML-compatible 128-byte format. + * Format: [private_key(32)] + [IV(32)] + [public_key(32)] + [chain_code(32)] + * This matches the format expected by CML.Bip32PrivateKey.from_128_xprv() + * + * @since 2.0.0 + * @category cml-compatibility + */ +export const to128XPRV = (bip32PrivateKey: Bip32PrivateKey): Uint8Array => + Eff.runSync(Effect.to_128_xprv(bip32PrivateKey)) + +/** + * Create Bip32PrivateKey from CML-compatible 128-byte format. + * Format: [private_key(32)] + [IV(32)] + [public_key(32)] + [chain_code(32)] + * This matches the format returned by CML.Bip32PrivateKey.to_128_xprv() + * + * @since 2.0.0 + * @category cml-compatibility + */ +export const from128XPRV = (bytes: Uint8Array): Bip32PrivateKey => + Eff.runSync(Effect.from_128_xprv(bytes)) + +// ============================================================================ +// Public Key Derivation +// ============================================================================ + +/** + * Derive the public key from this BIP32 private key. + * Uses the scalar part for Ed25519 point multiplication. + * + * @since 2.0.0 + * @category cryptography + */ +export const toPublicKey = (bip32PrivateKey: Bip32PrivateKey): Bip32PublicKey.Bip32PublicKey => + Eff.runSync(Effect.toPublicKey(bip32PrivateKey)) + +// ============================================================================ +// FastCheck Arbitrary +// ============================================================================ + +/** + * FastCheck arbitrary for generating random Bip32PrivateKey instances for testing. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.uint8Array({ + minLength: 96, + maxLength: 96 +}).map((bytes) => Eff.runSync(Effect.fromBytes(bytes))) + +// ============================================================================ +// Cardano-specific utilities +// ============================================================================ + +/** + * Cardano BIP44 derivation path utilities for BIP32 keys. + * + * @since 2.0.0 + * @category cardano + */ +export const CardanoPath = { + /** + * Create a Cardano BIP44 derivation path as indices array. + * Standard path: [1852', 1815', account', role, index] + */ + indices: (account: number = 0, role: 0 | 2 = 0, index: number = 0): Array => [ + 0x80000000 + 1852, // Purpose: 1852' (hardened) + 0x80000000 + 1815, // Coin type: ADA (hardened) + 0x80000000 + account, // Account (hardened) + role, // Role: 0=payment, 2=stake (not hardened) + index // Index (not hardened) + ], + + /** + * Payment key indices (role = 0) + */ + paymentIndices: (account: number = 0, index: number = 0) => + CardanoPath.indices(account, 0, index), + + /** + * Stake key indices (role = 2) + */ + stakeIndices: (account: number = 0, index: number = 0) => + CardanoPath.indices(account, 2, index) +} + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse a Bip32PrivateKey from raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.gen(function* () { + if (bytes.length !== 96) { + return yield* Eff.fail( + new Bip32PrivateKeyError({ + message: `Expected 96 bytes, got ${bytes.length} bytes` + }) + ) + } + return yield* Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => + new Bip32PrivateKeyError({ + message: "Failed to parse Bip32PrivateKey from bytes", + cause + }) + ) + }) + + /** + * Parse a Bip32PrivateKey from a hex string using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Eff.mapError( + Schema.decode(Bip32PrivateKey)(hex), + (cause) => + new Bip32PrivateKeyError({ + message: "Failed to parse Bip32PrivateKey from hex", + cause + }) + ) + + /** + * Create a Bip32PrivateKey from BIP39 entropy using Effect error handling. + * + * @since 2.0.0 + * @category bip39 + */ + export const fromBip39Entropy = ( + entropy: Uint8Array, + password: string = "" + ): Eff.Effect => + Eff.gen(function* () { + const keyMaterial = yield* Eff.try(() => + pbkdf2(sha512, password, entropy, { c: PBKDF2_ITERATIONS, dkLen: PBKDF2_KEY_SIZE }) + ) + + // Clamp the scalar part (first 32 bytes) + const clamped = new Uint8Array(keyMaterial) + clamped.set(clampScalar(keyMaterial.slice(0, 32)), 0) + + return yield* Schema.decode(FromBytes)(clamped) + }).pipe( + Eff.mapError( + (cause) => + new Bip32PrivateKeyError({ + message: "Failed to generate Bip32PrivateKey from BIP39 entropy", + cause + }) + ) + ) + + /** + * Convert a Bip32PrivateKey to raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (bip32PrivateKey: Bip32PrivateKey): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(bip32PrivateKey), + (cause) => + new Bip32PrivateKeyError({ + message: "Failed to encode Bip32PrivateKey to bytes", + cause + }) + ) + + /** + * Derive a child private key using a single index with Effect error handling. + * + * @since 2.0.0 + * @category bip32 + */ + export const deriveChild = ( + bip32PrivateKey: Bip32PrivateKey, + index: number + ): Eff.Effect => + Eff.gen(function* () { + const keyBytes = yield* Schema.encode(FromBytes)(bip32PrivateKey) + + // For soft derivation, we need the computed public key bytes, not the Bip32PublicKey + const computedPublicKeyBytes = yield* Eff.try(() => { + const scalar = extractScalar(keyBytes) + return sodium.crypto_scalarmult_ed25519_base_noclamp(scalar) + }) + + const derivedBytes = yield* Eff.try(() => { + const scalar = keyBytes.slice(0, 32) // First 32 bytes: scalar + const iv = keyBytes.slice(32, 64) // Second 32 bytes: IV + const chainCode = extractChainCode(keyBytes) + + // Determine if this is hardened or soft derivation + const isHardened = index >= 0x80000000 + const actualIndex = index // Use the actual index, don't force hardened + + // Serialize index in little-endian (V2 scheme) - CML compatible + const indexBytes = new Uint8Array(4) + indexBytes[0] = actualIndex & 0xff + indexBytes[1] = (actualIndex >>> 8) & 0xff + indexBytes[2] = (actualIndex >>> 16) & 0xff + indexBytes[3] = (actualIndex >>> 24) & 0xff + + // Create HMAC input for Z - use appropriate tag and key material + let zInput: Uint8Array + if (isHardened) { + // Hardened derivation: tag(0x00) + scalar(32) + iv(32) + index(4 bytes) + const zTag = new Uint8Array([0x00]) // TAG_DERIVE_Z_HARDENED + zInput = new Uint8Array(1 + 32 + 32 + 4) + zInput.set(zTag, 0) + zInput.set(scalar, 1) + zInput.set(iv, 33) + zInput.set(indexBytes, 65) + } else { + // Soft derivation: tag(0x02) + public_key(32 bytes) + index(4 bytes) + const zTag = new Uint8Array([0x02]) // TAG_DERIVE_Z_SOFT - try 0x02 + zInput = new Uint8Array(1 + 32 + 4) + zInput.set(zTag, 0) + zInput.set(computedPublicKeyBytes, 1) + zInput.set(indexBytes, 33) + } + + // HMAC-SHA512 with chain code as key + const hmacZ = sodium.crypto_auth_hmacsha512(zInput, chainCode) + + const z = new Uint8Array(hmacZ) + const zl = z.slice(0, 32) + const zr = z.slice(32, 64) + + // multiply8_v2: multiply by 8 using DERIVATION_V2 scheme (CML compatible) + // This implements add_28_mul8_v2(kl, zl) where kl is the scalar (left half) + const kl = scalar // Use the scalar, not the first 32 bytes of 64-byte "private key" + const scaledLeft = new Uint8Array(32) + let carry = 0 + // First 28 bytes: kl[i] + (zl[i] << 3) + carry + for (let i = 0; i < 28; i++) { + const r = kl[i] + (zl[i] << 3) + carry + scaledLeft[i] = r & 0xff + carry = r >> 8 + } + // Last 4 bytes: kl[i] + carry (no shift) + for (let i = 28; i < 32; i++) { + const r = kl[i] + carry + scaledLeft[i] = r & 0xff + carry = r >> 8 + } + + // scalar_add_no_overflow: The left half is already computed in scaledLeft + const newKeyMaterial = new Uint8Array(64) + // Use the computed scaledLeft directly for left half (new scalar) + newKeyMaterial.set(scaledLeft, 0) + + // Add right half (zr + iv) - the IV becomes the new right half + let carryBit = 0 + for (let i = 0; i < 32; i++) { + const sum = iv[i] + zr[i] + carryBit + newKeyMaterial[i + 32] = sum & 0xff + carryBit = sum > 255 ? 1 : 0 + } + + // Derive new chain code: use appropriate tag and key material + let ccInput: Uint8Array + if (isHardened) { + // Hardened derivation: tag(0x01) + scalar(32) + iv(32) + index(4 bytes) + const ccTag = new Uint8Array([0x01]) // TAG_DERIVE_CC_HARDENED + ccInput = new Uint8Array(1 + 32 + 32 + 4) + ccInput.set(ccTag, 0) + ccInput.set(scalar, 1) + ccInput.set(iv, 33) + ccInput.set(indexBytes, 65) + } else { + // Soft derivation: tag(0x03) + public_key(32 bytes) + index(4 bytes) + const ccTag = new Uint8Array([0x03]) // TAG_DERIVE_CC_SOFT - corrected to 0x03 + ccInput = new Uint8Array(1 + 32 + 4) + ccInput.set(ccTag, 0) + ccInput.set(computedPublicKeyBytes, 1) + ccInput.set(indexBytes, 33) + } + + const hmacCC = sodium.crypto_auth_hmacsha512(ccInput, chainCode) + const newChainCode = new Uint8Array(hmacCC).slice(32, 64) // Take right 32 bytes + + // Construct the new key: newKeyMaterial(64 bytes) + newChainCode(32 bytes) = 96 bytes + return new Uint8Array([...newKeyMaterial, ...newChainCode]) + }) + + return yield* Schema.decode(FromBytes)(derivedBytes) + }).pipe( + Eff.mapError( + (cause) => + new Bip32PrivateKeyError({ + message: `Failed to derive child key with index ${index}`, + cause + }) + ) + ) + + /** + * Derive a child private key using multiple indices with Effect error handling. + * + * @since 2.0.0 + * @category bip32 + */ + export const derive = ( + bip32PrivateKey: Bip32PrivateKey, + indices: Array + ): Eff.Effect => + Eff.gen(function* () { + let currentKey = bip32PrivateKey + + for (const index of indices) { + currentKey = yield* deriveChild(currentKey, index) + } + + return currentKey + }) + + /** + * Parse a BIP32 derivation path string into indices array. + * + * @since 2.0.0 + * @category bip32 + */ + const parsePath = (path: string): Eff.Effect, Bip32PrivateKeyError> => + Eff.try(() => { + const cleanPath = path.startsWith("m/") ? path.slice(2) : path + const segments = cleanPath.split("/") + + return segments.map(segment => { + const isHardened = segment.endsWith("'") || segment.endsWith("h") + const indexStr = isHardened ? segment.slice(0, -1) : segment + const index = parseInt(indexStr, 10) + + if (isNaN(index)) { + throw new Error(`Invalid path segment: ${segment}`) + } + + return isHardened ? 0x80000000 + index : index + }) + }).pipe( + Eff.mapError( + (cause) => + new Bip32PrivateKeyError({ + message: `Failed to parse derivation path: ${path}`, + cause + }) + ) + ) + + /** + * Derive a child private key using a path string with Effect error handling. + * + * @since 2.0.0 + * @category bip32 + */ + export const derivePath = ( + bip32PrivateKey: Bip32PrivateKey, + path: string + ): Eff.Effect => + Eff.gen(function* () { + const indices = yield* parsePath(path) + return yield* derive(bip32PrivateKey, indices) + }) + + /** + * Convert a Bip32PrivateKey to a standard PrivateKey using Effect error handling. + * + * @since 2.0.0 + * @category conversion + */ + export const toPrivateKey = (bip32PrivateKey: Bip32PrivateKey): Eff.Effect => + Eff.gen(function* () { + const keyBytes = yield* toBytes(bip32PrivateKey) + const privateKeyBytes = keyBytes.slice(0, 64) // scalar + IV + + return yield* Eff.mapError( + Schema.decode(PrivateKey.FromBytes)(privateKeyBytes), + (cause) => + new Bip32PrivateKeyError({ + message: "Failed to convert Bip32PrivateKey to PrivateKey", + cause + }) + ) + }) + + /** + * Derive the public key from this BIP32 private key using Effect error handling. + * + * @since 2.0.0 + * @category cryptography + */ + export const toPublicKey = (bip32PrivateKey: Bip32PrivateKey): Eff.Effect => + Eff.gen(function* () { + const keyBytes = yield* Schema.encode(FromBytes)(bip32PrivateKey) + + const publicKeyBytes = yield* Eff.try(() => { + const scalar = extractScalar(keyBytes) + return sodium.crypto_scalarmult_ed25519_base_noclamp(scalar) + }) + + const chainCode = extractChainCode(keyBytes) + + return yield* Eff.mapError( + Bip32PublicKey.Effect.fromBytes(publicKeyBytes, chainCode), + (cause) => + new Bip32PrivateKeyError({ + message: "Failed to create Bip32PublicKey", + cause + }) + ) + }).pipe( + Eff.mapError( + (cause) => + new Bip32PrivateKeyError({ + message: "Failed to derive public key from Bip32PrivateKey", + cause + }) + ) + ) + + // ============================================================================ + // CML Compatibility Methods + // ============================================================================ + + /** + * Serialize Bip32PrivateKey to CML-compatible 128-byte format using Effect error handling. + * Format: [private_key(32)] + [IV(32)] + [public_key(32)] + [chain_code(32)] + * This matches the format expected by CML.Bip32PrivateKey.from_128_xprv() + * + * @since 2.0.0 + * @category cml-compatibility + */ + export const to_128_xprv = (bip32PrivateKey: Bip32PrivateKey): Eff.Effect => + Eff.gen(function* () { + const keyBytes = yield* Eff.mapError( + Schema.encode(FromBytes)(bip32PrivateKey), + (cause) => new Bip32PrivateKeyError({ message: "Failed to encode key bytes", cause }) + ) + const publicKey = yield* toPublicKey(bip32PrivateKey) + const publicKeyBytes = yield* Eff.mapError( + Bip32PublicKey.Effect.toBytes(publicKey), + (cause) => new Bip32PrivateKeyError({ message: "Failed to get public key bytes", cause }) + ) + + // Extract components from our 96-byte format: [scalar(32)] + [IV(32)] + [chaincode(32)] + const scalar = keyBytes.slice(0, 32) + const iv = keyBytes.slice(32, 64) + const chaincode = keyBytes.slice(64, 96) + + // Extract just the public key part (first 32 bytes) from the public key bytes + const publicKeyOnly = publicKeyBytes.slice(0, 32) + + // Construct CML's 128-byte format: [scalar(32)] + [IV(32)] + [public_key(32)] + [chaincode(32)] + const cmlFormat = new Uint8Array(128) + cmlFormat.set(scalar, 0) // Bytes 0-31: private key + cmlFormat.set(iv, 32) // Bytes 32-63: IV/extension + cmlFormat.set(publicKeyOnly, 64) // Bytes 64-95: public key + cmlFormat.set(chaincode, 96) // Bytes 96-127: chain code + + return cmlFormat + }) + + /** + * Create Bip32PrivateKey from CML-compatible 128-byte format using Effect error handling. + * Format: [private_key(32)] + [IV(32)] + [public_key(32)] + [chain_code(32)] + * This matches the format returned by CML.Bip32PrivateKey.to_128_xprv() + * + * @since 2.0.0 + * @category cml-compatibility + */ + export const from_128_xprv = (bytes: Uint8Array): Eff.Effect => + Eff.gen(function* () { + if (bytes.length !== 128) { + return yield* Eff.fail( + new Bip32PrivateKeyError({ + message: `Expected exactly 128 bytes for CML format, got ${bytes.length}` + }) + ) + } + + // Extract components from CML's 128-byte format + const scalar = bytes.slice(0, 32) // Bytes 0-31: private key + const iv = bytes.slice(32, 64) // Bytes 32-63: IV/extension + const chaincode = bytes.slice(96, 128) // Bytes 96-127: chain code + + // Verify the public key matches the private key + const expectedPublicKey = bytes.slice(64, 96) // Bytes 64-95: public key + const derivedPublicKey = yield* Eff.try(() => + sodium.crypto_scalarmult_ed25519_base_noclamp(scalar) + ).pipe( + Eff.mapError((cause) => new Bip32PrivateKeyError({ message: "Failed to derive public key", cause })) + ) + + const publicKeyMatches = derivedPublicKey.every((byte, i) => byte === expectedPublicKey[i]) + if (!publicKeyMatches) { + return yield* Eff.fail( + new Bip32PrivateKeyError({ + message: "Public key does not match private key in 128-byte format" + }) + ) + } + + // Construct our internal 96-byte format: [scalar(32)] + [IV(32)] + [chaincode(32)] + const internalFormat = new Uint8Array(96) + internalFormat.set(scalar, 0) // Bytes 0-31: scalar + internalFormat.set(iv, 32) // Bytes 32-63: IV + internalFormat.set(chaincode, 64) // Bytes 64-95: chaincode + + return yield* Eff.mapError( + Schema.decode(FromBytes)(internalFormat), + (cause) => new Bip32PrivateKeyError({ + message: "Failed to decode internal format", + cause + }) + ) + }) +} diff --git a/packages/evolution/src/Bip32PublicKey.ts b/packages/evolution/src/Bip32PublicKey.ts new file mode 100644 index 00000000..798394a9 --- /dev/null +++ b/packages/evolution/src/Bip32PublicKey.ts @@ -0,0 +1,363 @@ +import { Data, Effect as Eff, FastCheck, Schema } from "effect" +import sodium from "libsodium-wrappers-sumo" + +import * as Bytes64 from "./Bytes64.js" + +// Initialize libsodium +await sodium.ready + +/** + * Error class for Bip32PublicKey related operations. + * + * @since 2.0.0 + * @category errors + */ +export class Bip32PublicKeyError extends Data.TaggedError("Bip32PublicKeyError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Schema for Bip32PublicKey representing a BIP32-Ed25519 extended public key. + * Always 64 bytes: 32-byte public key + 32-byte chaincode. + * Follows BIP32-Ed25519 hierarchical deterministic key derivation. + * Supports soft derivation only (hardened derivation requires private key). + * + * @since 2.0.0 + * @category schemas + */ +export const Bip32PublicKey = Bytes64.HexSchema + .pipe(Schema.brand("Bip32PublicKey")) + .annotations({ + identifier: "Bip32PublicKey" + }) + +export type Bip32PublicKey = typeof Bip32PublicKey.Type + +export const FromBytes = Schema.compose( + Bytes64.FromBytes, // Uint8Array -> hex string + Bip32PublicKey // hex string -> Bip32PublicKey +).annotations({ + identifier: "Bip32PublicKey.Bytes" +}) + +export const FromHex = Schema.compose( + Bytes64.HexSchema, // string -> hex string + Bip32PublicKey // hex string -> Bip32PublicKey +).annotations({ + identifier: "Bip32PublicKey.Hex" +}) + +/** + * Smart constructor for Bip32PublicKey that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = Bip32PublicKey.make + +/** + * Check if two Bip32PublicKey instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: Bip32PublicKey, b: Bip32PublicKey): boolean => a === b + +// Helper functions for extracting data from the 64-byte format + +/** + * Extract the public key (first 32 bytes) from a Bip32PublicKey. + * + * @since 2.0.0 + * @category accessors + */ +const extractPublicKey = (keyBytes: Uint8Array): Uint8Array => { + if (keyBytes.length !== 64) { + throw new Error(`Expected 64 bytes for Bip32PublicKey, got ${keyBytes.length}`) + } + return keyBytes.slice(0, 32) +} + +/** + * Extract the chain code (last 32 bytes) from a Bip32PublicKey. + * + * @since 2.0.0 + * @category accessors + */ +const extractChainCode = (keyBytes: Uint8Array): Uint8Array => { + if (keyBytes.length !== 64) { + throw new Error(`Expected 64 bytes for Bip32PublicKey, got ${keyBytes.length}`) + } + return keyBytes.slice(32, 64) +} + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Create a BIP32 public key from public key and chain code bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (publicKey: Uint8Array, chainCode: Uint8Array): Bip32PublicKey => { + if (publicKey.length !== 32) { + throw new Error(`Public key must be 32 bytes, got ${publicKey.length}`) + } + if (chainCode.length !== 32) { + throw new Error(`Chain code must be 32 bytes, got ${chainCode.length}`) + } + + const result = new Uint8Array(64) + result.set(publicKey, 0) + result.set(chainCode, 32) + + return Eff.runSync(Schema.decode(FromBytes)(result)) +} + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert a Bip32PublicKey to raw bytes (64 bytes). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (bip32PublicKey: Bip32PublicKey): Uint8Array => + Eff.runSync(Effect.toBytes(bip32PublicKey)) + +/** + * Convert a Bip32PublicKey to raw public key bytes (32 bytes only). + * + * @since 2.0.0 + * @category encoding + */ +export const toRawBytes = (bip32PublicKey: Bip32PublicKey): Uint8Array => { + const keyBytes = toBytes(bip32PublicKey) + return extractPublicKey(keyBytes) +} + +// ============================================================================ +// Derivation Functions +// ============================================================================ + +/** + * Derive a child public key using the specified index (soft derivation only). + * + * @since 2.0.0 + * @category derivation + */ +export const deriveChild = (bip32PublicKey: Bip32PublicKey, index: number): Bip32PublicKey => + Eff.runSync(Effect.deriveChild(bip32PublicKey, index)) + +// ============================================================================ +// Accessor Functions +// ============================================================================ + +/** + * Get the chain code. + * + * @since 2.0.0 + * @category accessors + */ +export const chainCode = (bip32PublicKey: Bip32PublicKey): Uint8Array => { + const keyBytes = toBytes(bip32PublicKey) + return extractChainCode(keyBytes) +} + +/** + * Get the public key bytes. + * + * @since 2.0.0 + * @category accessors + */ +export const publicKey = (bip32PublicKey: Bip32PublicKey): Uint8Array => { + const keyBytes = toBytes(bip32PublicKey) + return extractPublicKey(keyBytes) +} + +// ============================================================================ +// FastCheck Arbitrary +// ============================================================================ + +/** + * FastCheck arbitrary for generating random Bip32PublicKey instances for testing. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.uint8Array({ + minLength: 64, + maxLength: 64 +}).map((bytes) => Eff.runSync(Effect.fromBytes(bytes.slice(0, 32), bytes.slice(32, 64)))) + +/** + * @since 2.0.0 + * @category Effect + */ +export namespace Effect { + /** + * Create a BIP32 public key from public key and chain code bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = ( + publicKey: Uint8Array, + chainCode: Uint8Array + ): Eff.Effect => + Eff.gen(function* () { + if (publicKey.length !== 32) { + return yield* Eff.fail( + new Bip32PublicKeyError({ + message: `Public key must be 32 bytes, got ${publicKey.length}` + }) + ) + } + if (chainCode.length !== 32) { + return yield* Eff.fail( + new Bip32PublicKeyError({ + message: `Chain code must be 32 bytes, got ${chainCode.length}` + }) + ) + } + + const result = new Uint8Array(64) + result.set(publicKey, 0) + result.set(chainCode, 32) + + return yield* Schema.decode(FromBytes)(result) + }).pipe( + Eff.mapError( + (cause) => + new Bip32PublicKeyError({ + message: "Failed to create Bip32PublicKey from bytes", + cause + }) + ) + ) + + /** + * Convert Bip32PublicKey to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (bip32PublicKey: Bip32PublicKey): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(bip32PublicKey), + (cause) => + new Bip32PublicKeyError({ + message: "Failed to encode Bip32PublicKey to bytes", + cause + }) + ) + + /** + * Derive a child public key using the specified index with Effect error handling. + * Only supports soft derivation (index < 0x80000000). + * + * @since 2.0.0 + * @category derivation + */ + export const deriveChild = ( + bip32PublicKey: Bip32PublicKey, + index: number + ): Eff.Effect => + Eff.gen(function* () { + if (index >= 0x80000000) { + return yield* Eff.fail( + new Bip32PublicKeyError({ + message: `Hardened derivation (index >= 0x80000000) not supported for public keys, got index ${index}` + }) + ) + } + + // Get the key bytes first + const keyBytes = yield* toBytes(bip32PublicKey) + const parentPublicKey = extractPublicKey(keyBytes) + const parentChainCode = extractChainCode(keyBytes) + + const derivedBytes = yield* Eff.try(() => { + // Serialize index in little-endian (V2 scheme) - CML compatible + const indexBytes = new Uint8Array(4) + indexBytes[0] = index & 0xff + indexBytes[1] = (index >>> 8) & 0xff + indexBytes[2] = (index >>> 16) & 0xff + indexBytes[3] = (index >>> 24) & 0xff + + // Create HMAC input for Z (soft derivation): tag(0x02) + public_key(32 bytes) + index(4 bytes) + const zTag = new Uint8Array([0x02]) // TAG_DERIVE_Z_SOFT + const zInput = new Uint8Array(1 + 32 + 4) + zInput.set(zTag, 0) + zInput.set(parentPublicKey, 1) + zInput.set(indexBytes, 33) + + // HMAC-SHA512 with chain code as key + const hmacZ = sodium.crypto_auth_hmacsha512(zInput, parentChainCode) + const z = new Uint8Array(hmacZ) + const zl = z.slice(0, 32) + + // For public key derivation, we need to compute: parentPublicKey + mul8(zl)*G + // where G is the Ed25519 base point and mul8(zl) applies the same 8-multiplication + // that's used in private key derivation (add_28_mul8_v2 algorithm) + + // Apply the same mul8 operation that private key derivation uses + // This is critical for compatibility - multiply first 28 bytes by 8 + const zl8 = new Uint8Array(32) + let carry = 0 + // First 28 bytes: zl[i] << 3 (multiply by 8) + for (let i = 0; i < 28; i++) { + const r = (zl[i] << 3) + carry + zl8[i] = r & 0xff + carry = r >> 8 + } + // Last 4 bytes: just carry (no multiplication) + for (let i = 28; i < 32; i++) { + const r = carry + zl8[i] = r & 0xff + carry = r >> 8 + } + + // Now compute zl8*G (scalar multiplication with base point using processed zl) + const zl8G = sodium.crypto_scalarmult_ed25519_base_noclamp(zl8) + + // Then add parentPublicKey + zl8G (point addition) + const childPublicKey = sodium.crypto_core_ed25519_add(parentPublicKey, zl8G) + + // Derive new chain code: tag(0x03) + public_key(32 bytes) + index(4 bytes) + const ccTag = new Uint8Array([0x03]) // TAG_DERIVE_CC_SOFT - corrected to 0x03 + const ccInput = new Uint8Array(1 + 32 + 4) + ccInput.set(ccTag, 0) + ccInput.set(parentPublicKey, 1) + ccInput.set(indexBytes, 33) + + const hmacCC = sodium.crypto_auth_hmacsha512(ccInput, parentChainCode) + const newChainCode = new Uint8Array(hmacCC).slice(32, 64) // Take right 32 bytes + + return { + publicKey: childPublicKey, + chainCode: newChainCode + } + }) + + // Create the new key bytes + const newKeyBytes = new Uint8Array(64) + newKeyBytes.set(derivedBytes.publicKey, 0) + newKeyBytes.set(derivedBytes.chainCode, 32) + + return yield* Schema.decode(FromBytes)(newKeyBytes) + }).pipe( + Eff.mapError( + (cause) => + new Bip32PublicKeyError({ + message: `Failed to derive child public key with index ${index}`, + cause + }) + ) + ) +} diff --git a/packages/evolution/src/Block.ts b/packages/evolution/src/Block.ts index 12b64e10..da751c1c 100644 --- a/packages/evolution/src/Block.ts +++ b/packages/evolution/src/Block.ts @@ -36,7 +36,7 @@ export class BlockClass extends Schema.TaggedClass()("Block", { // key: TransactionIndex.TransactionIndexSchema, // value: AuxiliaryData.AuxiliaryData, // }), - invalidTransactions: Schema.Array(TransactionIndex.TransactionIndexSchema) + invalidTransactions: Schema.Array(TransactionIndex.TransactionIndex) }) {} export type Block = Schema.Schema.Type diff --git a/packages/evolution/src/BlockBodyHash.ts b/packages/evolution/src/BlockBodyHash.ts index a7aa5b53..450f0e39 100644 --- a/packages/evolution/src/BlockBodyHash.ts +++ b/packages/evolution/src/BlockBodyHash.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" /** * Error class for BlockBodyHash related operations. @@ -22,7 +21,7 @@ export class BlockBodyHashError extends Data.TaggedError("BlockBodyHashError")<{ * @since 2.0.0 * @category schemas */ -export const BlockBodyHash = pipe(Bytes32.HexSchema, Schema.brand("BlockBodyHash")).annotations({ +export const BlockBodyHash = Bytes32.HexSchema.pipe(Schema.brand("BlockBodyHash")).annotations({ identifier: "BlockBodyHash" }) @@ -42,6 +41,14 @@ export const FromHex = Schema.compose( identifier: "BlockBodyHash.Hex" }) +/** + * Smart constructor for BlockBodyHash that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = BlockBodyHash.make + /** * Check if two BlockBodyHash instances are equal. * @@ -51,26 +58,140 @@ export const FromHex = Schema.compose( export const equals = (a: BlockBodyHash, b: BlockBodyHash): boolean => a === b /** - * Generate a random BlockBodyHash. + * Check if the given value is a valid BlockBodyHash + * + * @since 2.0.0 + * @category predicates + */ +export const isBlockBodyHash = Schema.is(BlockBodyHash) + +/** + * FastCheck arbitrary for generating random BlockBodyHash instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes32.Bytes32_BYTES_LENGTH, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes32.HEX_LENGTH, + maxLength: Bytes32.HEX_LENGTH +}).map((hex) => hex as BlockBodyHash) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for BlockBodyHash encoding and decoding operations. + * Parse BlockBodyHash from bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - BlockBodyHashError -) +export const fromBytes = (bytes: Uint8Array): BlockBodyHash => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse BlockBodyHash from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): BlockBodyHash => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode BlockBodyHash to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (blockBodyHash: BlockBodyHash): Uint8Array => + Eff.runSync(Effect.toBytes(blockBodyHash)) + +/** + * Encode BlockBodyHash to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (blockBodyHash: BlockBodyHash): string => + Eff.runSync(Effect.toHex(blockBodyHash)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse BlockBodyHash from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new BlockBodyHashError({ + message: "Failed to parse BlockBodyHash from bytes", + cause + }) + ) + ) + + /** + * Parse BlockBodyHash from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new BlockBodyHashError({ + message: "Failed to parse BlockBodyHash from hex", + cause + }) + ) + ) + + /** + * Encode BlockBodyHash to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (blockBodyHash: BlockBodyHash): Eff.Effect => + Schema.encode(FromBytes)(blockBodyHash).pipe( + Eff.mapError( + (cause) => + new BlockBodyHashError({ + message: "Failed to encode BlockBodyHash to bytes", + cause + }) + ) + ) + + /** + * Encode BlockBodyHash to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (blockBodyHash: BlockBodyHash): Eff.Effect => + Schema.encode(FromHex)(blockBodyHash).pipe( + Eff.mapError( + (cause) => + new BlockBodyHashError({ + message: "Failed to encode BlockBodyHash to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/BlockHeaderHash.ts b/packages/evolution/src/BlockHeaderHash.ts index f741c3c4..37121f9c 100644 --- a/packages/evolution/src/BlockHeaderHash.ts +++ b/packages/evolution/src/BlockHeaderHash.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" /** * Error class for BlockHeaderHash related operations. @@ -22,7 +21,7 @@ export class BlockHeaderHashError extends Data.TaggedError("BlockHeaderHashError * @since 2.0.0 * @category schemas */ -export const BlockHeaderHash = pipe(Bytes32.HexSchema, Schema.brand("BlockHeaderHash")).annotations({ +export const BlockHeaderHash = Bytes32.HexSchema.pipe(Schema.brand("BlockHeaderHash")).annotations({ identifier: "BlockHeaderHash" }) @@ -42,6 +41,14 @@ export const FromHex = Schema.compose( identifier: "BlockHeaderHash.Hex" }) +/** + * Smart constructor for BlockHeaderHash that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = BlockHeaderHash.make + /** * Check if two BlockHeaderHash instances are equal. * @@ -51,26 +58,140 @@ export const FromHex = Schema.compose( export const equals = (a: BlockHeaderHash, b: BlockHeaderHash): boolean => a === b /** - * Generate a random BlockHeaderHash. + * Check if the given value is a valid BlockHeaderHash + * + * @since 2.0.0 + * @category predicates + */ +export const isBlockHeaderHash = Schema.is(BlockHeaderHash) + +/** + * FastCheck arbitrary for generating random BlockHeaderHash instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes32.Bytes32_BYTES_LENGTH, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes32.HEX_LENGTH, + maxLength: Bytes32.HEX_LENGTH +}).map((hex) => hex as BlockHeaderHash) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for BlockHeaderHash encoding and decoding operations. + * Parse BlockHeaderHash from bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - BlockHeaderHashError -) +export const fromBytes = (bytes: Uint8Array): BlockHeaderHash => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse BlockHeaderHash from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): BlockHeaderHash => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode BlockHeaderHash to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (blockHeaderHash: BlockHeaderHash): Uint8Array => + Eff.runSync(Effect.toBytes(blockHeaderHash)) + +/** + * Encode BlockHeaderHash to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (blockHeaderHash: BlockHeaderHash): string => + Eff.runSync(Effect.toHex(blockHeaderHash)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse BlockHeaderHash from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new BlockHeaderHashError({ + message: "Failed to parse BlockHeaderHash from bytes", + cause + }) + ) + ) + + /** + * Parse BlockHeaderHash from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new BlockHeaderHashError({ + message: "Failed to parse BlockHeaderHash from hex", + cause + }) + ) + ) + + /** + * Encode BlockHeaderHash to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (blockHeaderHash: BlockHeaderHash): Eff.Effect => + Schema.encode(FromBytes)(blockHeaderHash).pipe( + Eff.mapError( + (cause) => + new BlockHeaderHashError({ + message: "Failed to encode BlockHeaderHash to bytes", + cause + }) + ) + ) + + /** + * Encode BlockHeaderHash to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (blockHeaderHash: BlockHeaderHash): Eff.Effect => + Schema.encode(FromHex)(blockHeaderHash).pipe( + Eff.mapError( + (cause) => + new BlockHeaderHashError({ + message: "Failed to encode BlockHeaderHash to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/ByronAddress.ts b/packages/evolution/src/ByronAddress.ts index 3e697f2f..1d547163 100644 --- a/packages/evolution/src/ByronAddress.ts +++ b/packages/evolution/src/ByronAddress.ts @@ -1,8 +1,19 @@ -import { Schema } from "effect" +import { Data, Effect as Eff, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as NetworkId from "./NetworkId.js" +/** + * Error class for ByronAddress related operations. + * + * @since 2.0.0 + * @category errors + */ +export class ByronAddressError extends Data.TaggedError("ByronAddressError")<{ + message?: string + cause?: unknown +}> {} + /** * Byron legacy address format * @@ -22,16 +33,40 @@ export class ByronAddress extends Schema.TaggedClass("ByronAddress } } -// /** -// * Byron legacy address has limited support -// * @since 2.0.0 -// */ -export const BytesSchema = Schema.transform(Schema.Uint8ArrayFromSelf, ByronAddress, { +/** + * Schema for encoding/decoding Byron addresses as bytes. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, ByronAddress, { strict: true, - encode: (_, toA) => Bytes.Codec.Encode.bytes(toA.bytes), - decode: (_, fromA) => - new ByronAddress({ - networkId: NetworkId.NetworkId.make(0), - bytes: Bytes.Codec.Decode.bytes(fromA) + encode: (_, __, ___, toA) => + ParseResult.decode(Bytes.FromHex)(toA.bytes), + decode: (_, __, ast, fromA) => + Eff.gen(function* () { + const hexString = yield* ParseResult.encode(Bytes.FromHex)(fromA) + return new ByronAddress({ + networkId: NetworkId.NetworkId.make(0), + bytes: hexString + }) }) }) + +/** + * Schema for encoding/decoding Byron addresses as hex strings. + * + * @since 2.0.0 + * @category schemas + */ +export const FromHex = Schema.compose(Bytes.FromHex, BytesSchema) + +/** + * Checks if two Byron addresses are equal. + * + * @since 2.0.0 + * @category utils + */ +export const equals = (a: ByronAddress, b: ByronAddress): boolean => { + return a.networkId === b.networkId && a.bytes === b.bytes +} diff --git a/packages/evolution/src/Bytes.ts b/packages/evolution/src/Bytes.ts index 9ca73a8c..05c4cc83 100644 --- a/packages/evolution/src/Bytes.ts +++ b/packages/evolution/src/Bytes.ts @@ -1,7 +1,5 @@ import { Data, Schema } from "effect" -import * as _Codec from "./Codec.js" - export class BytesError extends Data.TaggedError("BytesError")<{ message?: string cause?: unknown @@ -193,11 +191,3 @@ export const FromBytesLenient = Schema.transform(Schema.Uint8ArrayFromSelf, HexL }).annotations({ identifier: "Bytes.FromBytesLenient" }) - -export const Codec = _Codec.createEncoders( - { - bytes: FromBytes, - bytesLenient: FromBytesLenient - }, - BytesError -) diff --git a/packages/evolution/src/Bytes128.ts b/packages/evolution/src/Bytes128.ts new file mode 100644 index 00000000..28e90c94 --- /dev/null +++ b/packages/evolution/src/Bytes128.ts @@ -0,0 +1,123 @@ +import { Data, Effect as Eff, Schema } from "effect" + +import * as Bytes from "./Bytes.js" + +export class Bytes128Error extends Data.TaggedError("Bytes128Error")<{ + message?: string + cause?: unknown +}> {} + +export const BYTES_LENGTH = 128 +export const HEX_LENGTH = 256 + +/** + * Schema for Bytes128 bytes with 128-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( + Schema.filter((a) => a.length === BYTES_LENGTH) +).annotations({ + identifier: "Bytes128.Bytes", + title: "128-byte Array", + description: "A Uint8Array containing exactly 128 bytes", + message: (issue) => + `Bytes128 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(128).fill(0)], +}) + +/** + * Schema for Bytes128 hex strings with 256-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Bytes128.Hex", + title: "128-byte Hex String", + description: "A hexadecimal string representing exactly 128 bytes (256 characters)", + message: (issue) => + `Bytes128 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(256)], +}) + +/** + * Schema transformer for Bytes128 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes128-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const FromBytes = Schema.transform(BytesSchema, HexSchema, { + strict: true, + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes128.FromBytes", + title: "Bytes128 from Uint8Array", + description: "Transforms a 128-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" +}) + +/** + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Effect { + /** + * Parse Bytes128 from raw bytes using Effect error handling. + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new Bytes128Error({ + message: "Failed to parse Bytes128 from bytes", + cause + }) + ) + + /** + * Convert Bytes128 hex to raw bytes using Effect error handling. + */ + export const toBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(hex), + (cause) => new Bytes128Error({ + message: "Failed to encode Bytes128 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes128 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert Bytes128 hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => + Eff.runSync(Effect.toBytes(hex)) diff --git a/packages/evolution/src/Bytes16.ts b/packages/evolution/src/Bytes16.ts index d5323ba2..66561d65 100644 --- a/packages/evolution/src/Bytes16.ts +++ b/packages/evolution/src/Bytes16.ts @@ -1,36 +1,139 @@ -import { pipe, Schema } from "effect" +import { Data, Either as E, Schema } from "effect" import * as Bytes from "./Bytes.js" +export class Bytes16Error extends Data.TaggedError("Bytes16Error")<{ + message?: string + cause?: unknown +}> {} + export const BYTES_LENGTH = 16 export const HEX_LENGTH = 32 -export const BytesSchema = pipe( - Schema.Uint8ArrayFromSelf, - Schema.filter((a) => a.length === BYTES_LENGTH, { - message: (issue) => - `${issue.actual} must be a byte array of length ${BYTES_LENGTH}, but got ${(issue.actual as Uint8Array).length}`, - identifier: "Bytes16.Bytes" - }) -) - -export const HexSchema = pipe( - Bytes.HexSchema, - Schema.filter((a) => a.length === HEX_LENGTH, { - message: (issue) => - `${issue.actual} must be a hex string of length ${HEX_LENGTH}, but got ${(issue.actual as string).length}`, - identifier: "Bytes16.Hex" - }) -) - -export const FromHex = Schema.transform(HexSchema, BytesSchema, { - strict: true, - decode: (toI) => Bytes.Codec.Encode.bytes(toI), - encode: (fromA) => Bytes.Codec.Decode.bytes(fromA) +/** + * Schema for Bytes16 bytes with 16-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( + Schema.filter((a) => a.length === BYTES_LENGTH) +).annotations({ + identifier: "Bytes16.Bytes", + title: "16-byte Array", + description: "A Uint8Array containing exactly 16 bytes", + message: (issue) => + `Bytes16 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(16).fill(0)], +}) + +/** + * Schema for Bytes16 hex strings with 32-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Bytes16.Hex", + title: "16-byte Hex String", + description: "A hexadecimal string representing exactly 16 bytes (32 characters)", + message: (issue) => + `Bytes16 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(32)], }) +/** + * Schema transformer for Bytes16 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes16-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ export const FromBytes = Schema.transform(BytesSchema, HexSchema, { strict: true, - encode: (toI) => Bytes.Codec.Encode.bytes(toI), - decode: (fromA) => Bytes.Codec.Decode.bytes(fromA) + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes16.FromBytes", + title: "Bytes16 from Uint8Array", + description: "Transforms a 16-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) + +/** + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Either { + /** + * Parse Bytes16 from raw bytes using Either error handling. + */ + export const fromBytes = (bytes: Uint8Array): E.Either => + E.mapLeft( + Schema.decodeEither(FromBytes)(bytes), + (cause) => new Bytes16Error({ + message: "Failed to parse Bytes16 from bytes", + cause + }) + ) + + /** + * Convert Bytes16 hex to raw bytes using Either error handling. + */ + export const toBytes = (hex: string): E.Either => + E.mapLeft( + Schema.encodeEither(FromBytes)(hex), + (cause) => new Bytes16Error({ + message: "Failed to encode Bytes16 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes16 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => { + try { + return Schema.decodeSync(FromBytes)(bytes) + } catch (cause) { + throw new Bytes16Error({ + message: "Failed to parse Bytes16 from bytes", + cause + }) + } +} + +/** + * Convert Bytes16 hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => { + try { + return Schema.encodeSync(FromBytes)(hex) + } catch (cause) { + throw new Bytes16Error({ + message: "Failed to encode Bytes16 to bytes", + cause + }) + } +} diff --git a/packages/evolution/src/Bytes29.ts b/packages/evolution/src/Bytes29.ts index 15acb635..b3ce0651 100644 --- a/packages/evolution/src/Bytes29.ts +++ b/packages/evolution/src/Bytes29.ts @@ -1,28 +1,123 @@ -import { pipe, Schema } from "effect" +import { Data, Effect as Eff, Schema } from "effect" import * as Bytes from "./Bytes.js" +export class Bytes29Error extends Data.TaggedError("Bytes29Error")<{ + message?: string + cause?: unknown +}> {} + export const BYTES_LENGTH = 29 export const HEX_LENGTH = 58 -export const BytesSchema = pipe( - Schema.Uint8ArrayFromSelf, - Schema.filter((a) => a.length === BYTES_LENGTH, { - message: (issue) => `${issue.actual} must be a byte array of length ${BYTES_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes29.Bytes" - }) -) - -export const HexSchema = pipe( - Bytes.HexSchema, - Schema.filter((a) => a.length === HEX_LENGTH, { - message: (issue) => `${issue.actual} must be a hex string of length ${HEX_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes29.Hex" - }) -) - -export const BytesFromHex = Schema.transform(HexSchema, BytesSchema, { +/** + * Schema for Bytes29 bytes with 29-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( + Schema.filter((a) => a.length === BYTES_LENGTH) +).annotations({ + identifier: "Bytes29.Bytes", + title: "29-byte Array", + description: "A Uint8Array containing exactly 29 bytes", + message: (issue) => + `Bytes29 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(29).fill(0)], +}) + +/** + * Schema for Bytes29 hex strings with 58-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Bytes29.Hex", + title: "29-byte Hex String", + description: "A hexadecimal string representing exactly 29 bytes (58 characters)", + message: (issue) => + `Bytes29 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(58)], +}) + +/** + * Schema transformer for Bytes29 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes29-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const FromBytes = Schema.transform(BytesSchema, HexSchema, { strict: true, - decode: (toI) => Bytes.Codec.Encode.bytes(toI), - encode: (fromA) => Bytes.Codec.Decode.bytes(fromA) + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes29.FromBytes", + title: "Bytes29 from Uint8Array", + description: "Transforms a 29-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) + +/** + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Effect { + /** + * Parse Bytes29 from raw bytes using Effect error handling. + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new Bytes29Error({ + message: "Failed to parse Bytes29 from bytes", + cause + }) + ) + + /** + * Convert Bytes29 hex to raw bytes using Effect error handling. + */ + export const toBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(hex), + (cause) => new Bytes29Error({ + message: "Failed to encode Bytes29 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes29 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert Bytes29 hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => + Eff.runSync(Effect.toBytes(hex)) diff --git a/packages/evolution/src/Bytes32.ts b/packages/evolution/src/Bytes32.ts index 49d78995..eb199f97 100644 --- a/packages/evolution/src/Bytes32.ts +++ b/packages/evolution/src/Bytes32.ts @@ -1,7 +1,6 @@ -import { Data, Schema } from "effect" +import { Data, Either as E, Schema } from "effect" import * as Bytes from "./Bytes.js" -import * as _Codec from "./Codec.js" export class Bytes32Error extends Data.TaggedError("Bytes32Error")<{ message?: string @@ -9,8 +8,8 @@ export class Bytes32Error extends Data.TaggedError("Bytes32Error")<{ }> {} // Add constants following the style guide -export const Bytes32_BYTES_LENGTH = 32 -export const Bytes32_HEX_LENGTH = 64 +export const BYTES_LENGTH = 32 +export const HEX_LENGTH = 64 /** * Schema for Bytes32 bytes with 32-byte length validation. @@ -19,11 +18,14 @@ export const Bytes32_HEX_LENGTH = 64 * @category schemas */ export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( - Schema.filter((a) => a.length === Bytes32_BYTES_LENGTH) + Schema.filter((a) => a.length === BYTES_LENGTH) ).annotations({ identifier: "Bytes32.Bytes", + title: "32-byte Array", + description: "A Uint8Array containing exactly 32 bytes", message: (issue) => - `Bytes32 bytes must be exactly ${Bytes32_BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}` + `Bytes32 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(32).fill(0)], }) /** @@ -32,10 +34,15 @@ export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( * @since 2.0.0 * @category schemas */ -export const HexSchema = Bytes.HexSchema.pipe(Schema.filter((a) => a.length === Bytes32_HEX_LENGTH)).annotations({ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ identifier: "Bytes32.Hex", + title: "32-byte Hex String", + description: "A hexadecimal string representing exactly 32 bytes (64 characters)", message: (issue) => - `Bytes32 hex must be exactly ${Bytes32_HEX_LENGTH} characters, got ${(issue.actual as string).length}` + `Bytes32 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(64)], }) /** @@ -46,10 +53,10 @@ export const HexSchema = Bytes.HexSchema.pipe(Schema.filter((a) => a.length === * @category schemas */ export const VariableBytesSchema = Schema.Uint8ArrayFromSelf.pipe( - Schema.filter((a) => a.length >= 0 && a.length <= Bytes32_BYTES_LENGTH) + Schema.filter((a) => a.length >= 0 && a.length <= BYTES_LENGTH) ).annotations({ message: (issue) => - `must be a byte array of length 0 to ${Bytes32_BYTES_LENGTH}, but got ${(issue.actual as Uint8Array).length}`, + `must be a byte array of length 0 to ${BYTES_LENGTH}, but got ${(issue.actual as Uint8Array).length}`, identifier: "Bytes32.VariableBytes" }) @@ -61,10 +68,10 @@ export const VariableBytesSchema = Schema.Uint8ArrayFromSelf.pipe( * @category schemas */ export const VariableHexSchema = Bytes.HexSchema.pipe( - Schema.filter((a) => a.length >= 0 && a.length <= Bytes32_HEX_LENGTH) + Schema.filter((a) => a.length >= 0 && a.length <= HEX_LENGTH) ).annotations({ message: (issue) => - `must be a hex string of length 0 to ${Bytes32_HEX_LENGTH}, but got ${(issue.actual as string).length}`, + `must be a hex string of length 0 to ${HEX_LENGTH}, but got ${(issue.actual as string).length}`, identifier: "Bytes32.VariableHex" }) @@ -91,6 +98,11 @@ export const FromBytes = Schema.transform(BytesSchema, HexSchema, { } return array } +}).annotations({ + identifier: "Bytes32.FromBytes", + title: "Bytes32 from Uint8Array", + description: "Transforms a 32-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) /** @@ -117,18 +129,131 @@ export const FromVariableBytes = Schema.transform(VariableBytesSchema, VariableH } return array } +}).annotations({ + identifier: "Bytes32.FromVariableBytes", + title: "Variable Bytes32 from Uint8Array", + description: "Transforms variable-length byte arrays (0-32 bytes) to hex strings (0-64 chars)", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) /** - * Codec for Bytes32 encoding and decoding operations. + * Either namespace containing composable operations that can fail. + * All functions return Either objects for proper error handling and composition. + */ +export namespace Either { + /** + * Parse Bytes32 from raw bytes using Either error handling. + */ + export const fromBytes = (bytes: Uint8Array): E.Either => + E.mapLeft( + Schema.decodeEither(FromBytes)(bytes), + (cause) => new Bytes32Error({ + message: "Failed to parse Bytes32 from bytes", + cause + }) + ) + + /** + * Convert Bytes32 hex to raw bytes using Either error handling. + */ + export const toBytes = (hex: string): E.Either => + E.mapLeft( + Schema.encodeEither(FromBytes)(hex), + (cause) => new Bytes32Error({ + message: "Failed to encode Bytes32 to bytes", + cause + }) + ) + + /** + * Parse variable-length data from raw bytes using Either error handling. + */ + export const fromVariableBytes = (bytes: Uint8Array): E.Either => + E.mapLeft( + Schema.decodeEither(FromVariableBytes)(bytes), + (cause) => new Bytes32Error({ + message: "Failed to parse variable Bytes32 from bytes", + cause + }) + ) + + /** + * Convert variable-length hex to raw bytes using Either error handling. + */ + export const toVariableBytes = (hex: string): E.Either => + E.mapLeft( + Schema.encodeEither(FromVariableBytes)(hex), + (cause) => new Bytes32Error({ + message: "Failed to encode variable Bytes32 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes32 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => { + try { + return Schema.decodeSync(FromBytes)(bytes) + } catch (cause) { + throw new Bytes32Error({ + message: "Failed to parse Bytes32 from bytes", + cause + }) + } +} + +/** + * Convert Bytes32 hex to raw bytes (unsafe - throws on error). * * @since 2.0.0 - * @category encoding/decoding + * @category encoding */ -export const Codec = _Codec.createEncoders( - { - bytes: FromBytes, - variableBytes: FromVariableBytes - }, - Bytes32Error -) +export const toBytes = (hex: string): Uint8Array => { + try { + return Schema.encodeSync(FromBytes)(hex) + } catch (cause) { + throw new Bytes32Error({ + message: "Failed to encode Bytes32 to bytes", + cause + }) + } +} + +/** + * Parse variable-length data from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromVariableBytes = (bytes: Uint8Array): string => { + try { + return Schema.decodeSync(FromVariableBytes)(bytes) + } catch (cause) { + throw new Bytes32Error({ + message: "Failed to parse variable Bytes32 from bytes", + cause + }) + } +} + +/** + * Convert variable-length hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toVariableBytes = (hex: string): Uint8Array => { + try { + return Schema.encodeSync(FromVariableBytes)(hex) + } catch (cause) { + throw new Bytes32Error({ + message: "Failed to encode variable Bytes32 to bytes", + cause + }) + } +} diff --git a/packages/evolution/src/Bytes4.ts b/packages/evolution/src/Bytes4.ts index e8f34c94..b7473b78 100644 --- a/packages/evolution/src/Bytes4.ts +++ b/packages/evolution/src/Bytes4.ts @@ -1,34 +1,139 @@ -import { pipe, Schema } from "effect" +import { Data, Either as E, Schema } from "effect" import * as Bytes from "./Bytes.js" +export class Bytes4Error extends Data.TaggedError("Bytes4Error")<{ + message?: string + cause?: unknown +}> {} + export const BYTES_LENGTH = 4 export const HEX_LENGTH = 8 -export const BytesSchema = pipe( - Schema.Uint8ArrayFromSelf, - Schema.filter((a) => a.length === BYTES_LENGTH, { - message: (issue) => `${issue.actual} must be a byte array of length ${BYTES_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes4.Bytes" - }) -) - -export const HexSchema = pipe( - Bytes.HexSchema, - Schema.filter((a) => a.length === HEX_LENGTH, { - message: (issue) => `${issue.actual} must be a hex string of length ${HEX_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes4.Hex" - }) -) - -export const FromHex = Schema.transform(HexSchema, BytesSchema, { - strict: true, - decode: (toI) => Bytes.Codec.Encode.bytes(toI), - encode: (fromA) => Bytes.Codec.Decode.bytes(fromA) +/** + * Schema for Bytes4 bytes with 4-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( + Schema.filter((a) => a.length === BYTES_LENGTH) +).annotations({ + identifier: "Bytes4.Bytes", + title: "4-byte Array", + description: "A Uint8Array containing exactly 4 bytes", + message: (issue) => + `Bytes4 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(4).fill(0)], +}) + +/** + * Schema for Bytes4 hex strings with 8-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Bytes4.Hex", + title: "4-byte Hex String", + description: "A hexadecimal string representing exactly 4 bytes (8 characters)", + message: (issue) => + `Bytes4 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(8)], }) +/** + * Schema transformer for Bytes4 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes4-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ export const FromBytes = Schema.transform(BytesSchema, HexSchema, { strict: true, - encode: (toI) => Bytes.Codec.Encode.bytes(toI), - decode: (fromA) => Bytes.Codec.Decode.bytes(fromA) + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes4.FromBytes", + title: "Bytes4 from Uint8Array", + description: "Transforms a 4-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) + +/** + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Either { + /** + * Parse Bytes4 from raw bytes using Either error handling. + */ + export const fromBytes = (bytes: Uint8Array): E.Either => + E.mapLeft( + Schema.decodeEither(FromBytes)(bytes), + (cause) => new Bytes4Error({ + message: "Failed to parse Bytes4 from bytes", + cause + }) + ) + + /** + * Convert Bytes4 hex to raw bytes using Either error handling. + */ + export const toBytes = (hex: string): E.Either => + E.mapLeft( + Schema.encodeEither(FromBytes)(hex), + (cause) => new Bytes4Error({ + message: "Failed to encode Bytes4 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes4 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => { + try { + return Schema.decodeSync(FromBytes)(bytes) + } catch (cause) { + throw new Bytes4Error({ + message: "Failed to parse Bytes4 from bytes", + cause + }) + } +} + +/** + * Convert Bytes4 hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => { + try { + return Schema.encodeSync(FromBytes)(hex) + } catch (cause) { + throw new Bytes4Error({ + message: "Failed to encode Bytes4 to bytes", + cause + }) + } +} diff --git a/packages/evolution/src/Bytes448.ts b/packages/evolution/src/Bytes448.ts index 837fc877..2ba67760 100644 --- a/packages/evolution/src/Bytes448.ts +++ b/packages/evolution/src/Bytes448.ts @@ -1,34 +1,123 @@ -import { pipe, Schema } from "effect" +import { Data, Effect as Eff, Schema } from "effect" import * as Bytes from "./Bytes.js" +export class Bytes448Error extends Data.TaggedError("Bytes448Error")<{ + message?: string + cause?: unknown +}> {} + export const BYTES_LENGTH = 448 export const HEX_LENGTH = 896 -export const BytesSchema = pipe( - Schema.Uint8ArrayFromSelf, - Schema.filter((a) => a.length === BYTES_LENGTH, { - message: (issue) => `${issue.actual} must be a byte array of length ${BYTES_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes448.Bytes" - }) -) - -export const HexSchema = pipe( - Bytes.HexSchema, - Schema.filter((a) => a.length === HEX_LENGTH, { - message: (issue) => `${issue.actual} must be a hex string of length ${HEX_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes448.Hex" - }) -) - -export const FromHex = Schema.transform(HexSchema, BytesSchema, { - strict: true, - decode: (toI) => Bytes.Codec.Encode.bytes(toI), - encode: (fromA) => Bytes.Codec.Decode.bytes(fromA) +/** + * Schema for Bytes448 bytes with 448-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( + Schema.filter((a) => a.length === BYTES_LENGTH) +).annotations({ + identifier: "Bytes448.Bytes", + title: "448-byte Array", + description: "A Uint8Array containing exactly 448 bytes", + message: (issue) => + `Bytes448 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(448).fill(0)], +}) + +/** + * Schema for Bytes448 hex strings with 896-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Bytes448.Hex", + title: "448-byte Hex String", + description: "A hexadecimal string representing exactly 448 bytes (896 characters)", + message: (issue) => + `Bytes448 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(896)], }) +/** + * Schema transformer for Bytes448 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes448-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ export const FromBytes = Schema.transform(BytesSchema, HexSchema, { strict: true, - encode: (toI) => Bytes.Codec.Encode.bytes(toI), - decode: (fromA) => Bytes.Codec.Decode.bytes(fromA) + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes448.FromBytes", + title: "Bytes448 from Uint8Array", + description: "Transforms a 448-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) + +/** + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Effect { + /** + * Parse Bytes448 from raw bytes using Effect error handling. + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new Bytes448Error({ + message: "Failed to parse Bytes448 from bytes", + cause + }) + ) + + /** + * Convert Bytes448 hex to raw bytes using Effect error handling. + */ + export const toBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(hex), + (cause) => new Bytes448Error({ + message: "Failed to encode Bytes448 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes448 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert Bytes448 hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => + Eff.runSync(Effect.toBytes(hex)) diff --git a/packages/evolution/src/Bytes57.ts b/packages/evolution/src/Bytes57.ts index 405db739..f7b7e0e7 100644 --- a/packages/evolution/src/Bytes57.ts +++ b/packages/evolution/src/Bytes57.ts @@ -1,28 +1,123 @@ -import { pipe, Schema } from "effect" +import { Data, Effect as Eff, Schema } from "effect" import * as Bytes from "./Bytes.js" +export class Bytes57Error extends Data.TaggedError("Bytes57Error")<{ + message?: string + cause?: unknown +}> {} + export const BYTES_LENGTH = 57 export const HEX_LENGTH = 114 -export const BytesSchema = pipe( - Schema.Uint8ArrayFromSelf, - Schema.filter((a) => a.length === BYTES_LENGTH, { - message: (issue) => `${issue.actual} must be a byte array of length ${BYTES_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes57.Bytes" - }) -) - -export const HexSchema = pipe( - Bytes.HexSchema, - Schema.filter((a) => a.length === HEX_LENGTH, { - message: (issue) => `${issue.actual} must be a hex string of length ${HEX_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes57.Hex" - }) -) - -export const BytesFromHex = Schema.transform(HexSchema, BytesSchema, { +/** + * Schema for Bytes57 bytes with 57-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( + Schema.filter((a) => a.length === BYTES_LENGTH) +).annotations({ + identifier: "Bytes57.Bytes", + title: "57-byte Array", + description: "A Uint8Array containing exactly 57 bytes", + message: (issue) => + `Bytes57 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(57).fill(0)], +}) + +/** + * Schema for Bytes57 hex strings with 114-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Bytes57.Hex", + title: "57-byte Hex String", + description: "A hexadecimal string representing exactly 57 bytes (114 characters)", + message: (issue) => + `Bytes57 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(114)], +}) + +/** + * Schema transformer for Bytes57 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes57-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const FromBytes = Schema.transform(BytesSchema, HexSchema, { strict: true, - decode: (toI) => Bytes.Codec.Encode.bytes(toI), - encode: (fromA) => Bytes.Codec.Decode.bytes(fromA) + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes57.FromBytes", + title: "Bytes57 from Uint8Array", + description: "Transforms a 57-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) + +/** + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Effect { + /** + * Parse Bytes57 from raw bytes using Effect error handling. + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new Bytes57Error({ + message: "Failed to parse Bytes57 from bytes", + cause + }) + ) + + /** + * Convert Bytes57 hex to raw bytes using Effect error handling. + */ + export const toBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(hex), + (cause) => new Bytes57Error({ + message: "Failed to encode Bytes57 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes57 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert Bytes57 hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => + Eff.runSync(Effect.toBytes(hex)) diff --git a/packages/evolution/src/Bytes64.ts b/packages/evolution/src/Bytes64.ts index 02301aed..b157cd7d 100644 --- a/packages/evolution/src/Bytes64.ts +++ b/packages/evolution/src/Bytes64.ts @@ -1,34 +1,148 @@ -import { pipe, Schema } from "effect" +import { Data, Effect as Eff, Schema } from "effect" import * as Bytes from "./Bytes.js" +export class Bytes64Error extends Data.TaggedError("Bytes64Error")<{ + message?: string + cause?: unknown +}> {} + +// Add constants following the style guide export const BYTES_LENGTH = 64 export const HEX_LENGTH = 128 -export const BytesSchema = pipe( - Schema.Uint8ArrayFromSelf, - Schema.filter((a) => a.length === BYTES_LENGTH, { - message: (issue) => `${issue.actual} must be a byte array of length ${BYTES_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes64.Bytes" - }) -) - -export const HexSchema = pipe( - Bytes.HexSchema, - Schema.filter((a) => a.length === HEX_LENGTH, { - message: (issue) => `${issue.actual} must be a hex string of length ${HEX_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes64.Hex" - }) -) - -export const FromHex = Schema.transform(HexSchema, BytesSchema, { - strict: true, - decode: (toI) => Bytes.Codec.Encode.bytes(toI), - encode: (fromA) => Bytes.Codec.Decode.bytes(fromA) +/** + * Schema for Bytes64 bytes with 64-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe(Schema.filter((a) => a.length === BYTES_LENGTH)).annotations({ + identifier: "Bytes64.Bytes", + title: "64-byte Array", + description: "A Uint8Array containing exactly 64 bytes", + message: (issue) => `Bytes64 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(64).fill(0)] +}) + +/** + * Schema for Bytes64 hex strings with 128-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe(Schema.filter((a) => a.length === HEX_LENGTH)).annotations({ + identifier: "Bytes64.Hex", + title: "64-byte Hex String", + description: "A hexadecimal string representing exactly 64 bytes (128 characters)", + message: (issue) => `Bytes64 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(128)] }) +/** + * Schema transformer for Bytes64 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes64-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ export const FromBytes = Schema.transform(BytesSchema, HexSchema, { strict: true, - encode: (toI) => Bytes.Codec.Encode.bytes(toI), - decode: (fromA) => Bytes.Codec.Decode.bytes(fromA) + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes64.FromBytes", + title: "Bytes64 from Uint8Array", + description: "Transforms a 64-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse Bytes64 from raw bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => { + try { + return Schema.decodeSync(FromBytes)(bytes) + } catch (cause) { + throw new Bytes64Error({ + message: "Failed to parse Bytes64 from bytes", + cause + }) + } +} + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert Bytes64 hex to raw bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => Eff.runSync(Effect.toBytes(hex)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Bytes64 from raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => + new Bytes64Error({ + message: "Failed to parse Bytes64 from bytes", + cause + }) + ) + + /** + * Convert Bytes64 hex to raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(hex), + (cause) => + new Bytes64Error({ + message: "Failed to encode Bytes64 to bytes", + cause + }) + ) +} diff --git a/packages/evolution/src/Bytes80.ts b/packages/evolution/src/Bytes80.ts index 10f415be..4e98baab 100644 --- a/packages/evolution/src/Bytes80.ts +++ b/packages/evolution/src/Bytes80.ts @@ -1,34 +1,123 @@ -import { pipe, Schema } from "effect" +import { Data, Effect as Eff, Schema } from "effect" import * as Bytes from "./Bytes.js" +export class Bytes80Error extends Data.TaggedError("Bytes80Error")<{ + message?: string + cause?: unknown +}> {} + export const BYTES_LENGTH = 80 export const HEX_LENGTH = 160 -export const BytesSchema = pipe( - Schema.Uint8ArrayFromSelf, - Schema.filter((a) => a.length === BYTES_LENGTH, { - message: (issue) => `${issue.actual} must be a byte array of length ${BYTES_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes80.Bytes" - }) -) - -export const HexSchema = pipe( - Bytes.HexSchema, - Schema.filter((a) => a.length === HEX_LENGTH, { - message: (issue) => `${issue.actual} must be a hex string of length ${HEX_LENGTH}, but got ${issue.actual}`, - identifier: "Bytes80.Hex" - }) -) - -export const FromHex = Schema.transform(HexSchema, BytesSchema, { - strict: true, - decode: (toI) => Bytes.Codec.Encode.bytes(toI), - encode: (fromA) => Bytes.Codec.Decode.bytes(fromA) +/** + * Schema for Bytes80 bytes with 80-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( + Schema.filter((a) => a.length === BYTES_LENGTH) +).annotations({ + identifier: "Bytes80.Bytes", + title: "80-byte Array", + description: "A Uint8Array containing exactly 80 bytes", + message: (issue) => + `Bytes80 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(80).fill(0)], +}) + +/** + * Schema for Bytes80 hex strings with 160-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Bytes80.Hex", + title: "80-byte Hex String", + description: "A hexadecimal string representing exactly 80 bytes (160 characters)", + message: (issue) => + `Bytes80 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(160)], }) +/** + * Schema transformer for Bytes80 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes80-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ export const FromBytes = Schema.transform(BytesSchema, HexSchema, { strict: true, - encode: (toI) => Bytes.Codec.Encode.bytes(toI), - decode: (fromA) => Bytes.Codec.Decode.bytes(fromA) + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes80.FromBytes", + title: "Bytes80 from Uint8Array", + description: "Transforms a 80-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) + +/** + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Effect { + /** + * Parse Bytes80 from raw bytes using Effect error handling. + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new Bytes80Error({ + message: "Failed to parse Bytes80 from bytes", + cause + }) + ) + + /** + * Convert Bytes80 hex to raw bytes using Effect error handling. + */ + export const toBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(hex), + (cause) => new Bytes80Error({ + message: "Failed to encode Bytes80 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes80 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert Bytes80 hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => + Eff.runSync(Effect.toBytes(hex)) diff --git a/packages/evolution/src/Bytes96.ts b/packages/evolution/src/Bytes96.ts new file mode 100644 index 00000000..f361d215 --- /dev/null +++ b/packages/evolution/src/Bytes96.ts @@ -0,0 +1,123 @@ +import { Data, Effect as Eff, Schema } from "effect" + +import * as Bytes from "./Bytes.js" + +export class Bytes96Error extends Data.TaggedError("Bytes96Error")<{ + message?: string + cause?: unknown +}> {} + +export const BYTES_LENGTH = 96 +export const HEX_LENGTH = 192 + +/** + * Schema for Bytes96 bytes with 96-byte length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( + Schema.filter((a) => a.length === BYTES_LENGTH) +).annotations({ + identifier: "Bytes96.Bytes", + title: "96-byte Array", + description: "A Uint8Array containing exactly 96 bytes", + message: (issue) => + `Bytes96 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(96).fill(0)], +}) + +/** + * Schema for Bytes96 hex strings with 192-character length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Bytes96.Hex", + title: "96-byte Hex String", + description: "A hexadecimal string representing exactly 96 bytes (192 characters)", + message: (issue) => + `Bytes96 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(192)], +}) + +/** + * Schema transformer for Bytes96 that converts between hex strings and byte arrays. + * Like Bytes.BytesSchema but with Bytes96-specific length validation. + * + * @since 2.0.0 + * @category schemas + */ +export const FromBytes = Schema.transform(BytesSchema, HexSchema, { + strict: true, + decode: (toA) => { + let hex = "" + for (let i = 0; i < toA.length; i++) { + hex += toA[i].toString(16).padStart(2, "0") + } + return hex + }, + encode: (fromA) => { + const array = new Uint8Array(fromA.length / 2) + for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { + array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) + } + return array + } +}).annotations({ + identifier: "Bytes96.FromBytes", + title: "Bytes96 from Uint8Array", + description: "Transforms a 96-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" +}) + +/** + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Effect { + /** + * Parse Bytes96 from raw bytes using Effect error handling. + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new Bytes96Error({ + message: "Failed to parse Bytes96 from bytes", + cause + }) + ) + + /** + * Convert Bytes96 hex to raw bytes using Effect error handling. + */ + export const toBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(hex), + (cause) => new Bytes96Error({ + message: "Failed to encode Bytes96 to bytes", + cause + }) + ) +} + +/** + * Parse Bytes96 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert Bytes96 hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (hex: string): Uint8Array => + Eff.runSync(Effect.toBytes(hex)) diff --git a/packages/evolution/src/CBOR.ts b/packages/evolution/src/CBOR.ts index 82e1d8e7..1359e02a 100644 --- a/packages/evolution/src/CBOR.ts +++ b/packages/evolution/src/CBOR.ts @@ -1,7 +1,6 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" -import * as _Codec from "./Codec.js" /** * Error class for CBOR value operations @@ -93,7 +92,22 @@ export const CANONICAL_OPTIONS: CodecOptions = { * @since 1.0.0 * @category constants */ -export const DEFAULT_OPTIONS: CodecOptions = { +export const CML_DEFAULT_OPTIONS: CodecOptions = { + mode: "custom", + useIndefiniteArrays: false, + useIndefiniteMaps: false, + useDefiniteForEmpty: true, + sortMapKeys: false, + useMinimalEncoding: true +} as const + +/** + * Default CBOR encoding option for Data + * + * @since 1.0.0 + * @category constants + */ +export const CML_DATA_DEFAULT_OPTIONS: CodecOptions = { mode: "custom", useIndefiniteArrays: true, useIndefiniteMaps: true, @@ -284,8 +298,8 @@ export const match = ( } // Internal encoding function used by Schema.transformOrFail -const internalEncode = (value: CBOR, options: CodecOptions = DEFAULT_OPTIONS): Effect.Effect => - Effect.gen(function* () { +const internalEncode = (value: CBOR, options: CodecOptions = CML_DEFAULT_OPTIONS): Eff.Effect => + Eff.gen(function* () { if (typeof value === "bigint") { if (value >= 0n) { return yield* encodeUint(value, options) @@ -331,8 +345,8 @@ const internalEncode = (value: CBOR, options: CodecOptions = DEFAULT_OPTIONS): E }) // Internal decoding function used by Schema.transformOrFail -const internalDecode = (data: Uint8Array): Effect.Effect => - Effect.gen(function* () { +const internalDecode = (data: Uint8Array): Eff.Effect => + Eff.gen(function* () { if (data.length === 0) { return yield* new CBORError({ message: "Empty CBOR data" }) } @@ -351,8 +365,8 @@ const internalDecode = (data: Uint8Array): Effect.Effect => // Internal encoding functions -const encodeUint = (value: bigint, options: CodecOptions): Effect.Effect => - Effect.gen(function* () { +const encodeUint = (value: bigint, options: CodecOptions): Eff.Effect => + Eff.gen(function* () { if (value < 0n) { return yield* new CBORError({ message: `Cannot encode negative value ${value} as unsigned integer` @@ -401,8 +415,8 @@ const encodeUint = (value: bigint, options: CodecOptions): Effect.Effect => - Effect.gen(function* () { +const encodeNint = (value: bigint, options: CodecOptions): Eff.Effect => + Eff.gen(function* () { if (value >= 0n) { return yield* new CBORError({ message: `Cannot encode non-negative value ${value} as negative integer` @@ -454,8 +468,8 @@ const encodeNint = (value: bigint, options: CodecOptions): Effect.Effect => - Effect.gen(function* () { +const encodeBytes = (value: Uint8Array, options: CodecOptions): Eff.Effect => + Eff.gen(function* () { const length = value.length let headerBytes: Uint8Array const useMinimal = options.mode === "canonical" || (options.mode === "custom" && options.useMinimalEncoding) @@ -486,8 +500,8 @@ const encodeBytes = (value: Uint8Array, options: CodecOptions): Effect.Effect => - Effect.gen(function* () { +const encodeText = (value: string, options: CodecOptions): Eff.Effect => + Eff.gen(function* () { const utf8Bytes = new TextEncoder().encode(value) const length = utf8Bytes.length let headerBytes: Uint8Array @@ -519,8 +533,8 @@ const encodeText = (value: string, options: CodecOptions): Effect.Effect, options: CodecOptions): Effect.Effect => - Effect.gen(function* () { +const encodeArray = (value: ReadonlyArray, options: CodecOptions): Eff.Effect => + Eff.gen(function* () { const length = value.length const chunks: Array = [] const useMinimal = options.mode === "canonical" || (options.mode === "custom" && options.useMinimalEncoding) @@ -575,8 +589,8 @@ const encodeArray = (value: ReadonlyArray, options: CodecOptions): Effect. return result }) -const encodeMap = (value: ReadonlyMap, options: CodecOptions): Effect.Effect => - Effect.gen(function* () { +const encodeMap = (value: ReadonlyMap, options: CodecOptions): Eff.Effect => + Eff.gen(function* () { // Convert Map to array of pairs for processing const pairs = Array.from(value.entries()) const length = pairs.length @@ -590,9 +604,9 @@ const encodeMap = (value: ReadonlyMap, options: CodecOptions): Effec if (sortKeys) { // Sort by encoded key length only (matches old CBOR.ts behavior) - const tempEncodedPairs = yield* Effect.all( + const tempEncodedPairs = yield* Eff.all( pairs.map(([key, val]) => - Effect.gen(function* () { + Eff.gen(function* () { const encodedKey = yield* internalEncode(key, options) const encodedValue = yield* internalEncode(val, options) return { encodedKey, encodedValue } @@ -682,8 +696,8 @@ const encodeMap = (value: ReadonlyMap, options: CodecOptions): Effec const encodeRecord = ( value: { readonly [key: string]: CBOR }, options: CodecOptions -): Effect.Effect => - Effect.gen(function* () { +): Eff.Effect => + Eff.gen(function* () { // Convert Record to array of pairs for processing const pairs = Object.entries(value) const length = pairs.length @@ -697,9 +711,9 @@ const encodeRecord = ( if (sortKeys) { // Sort by encoded key length only (matches old CBOR.ts behavior) - const tempEncodedPairs = yield* Effect.all( + const tempEncodedPairs = yield* Eff.all( pairs.map(([key, val]) => - Effect.gen(function* () { + Eff.gen(function* () { const encodedKey = yield* internalEncode(key, options) const encodedValue = yield* internalEncode(val, options) return { encodedKey, encodedValue } @@ -786,8 +800,8 @@ const encodeRecord = ( return result }) -const encodeTag = (tag: number, value: CBOR, options: CodecOptions): Effect.Effect => - Effect.gen(function* () { +const encodeTag = (tag: number, value: CBOR, options: CodecOptions): Eff.Effect => + Eff.gen(function* () { const chunks: Array = [] const useMinimal = options.mode === "canonical" || (options.mode === "custom" && options.useMinimalEncoding) @@ -818,8 +832,8 @@ const encodeTag = (tag: number, value: CBOR, options: CodecOptions): Effect.Effe return result }) -const encodeSimple = (value: boolean | null | undefined): Effect.Effect => - Effect.gen(function* () { +const encodeSimple = (value: boolean | null | undefined): Eff.Effect => + Eff.gen(function* () { if (value === false) return new Uint8Array([0xf4]) if (value === true) return new Uint8Array([0xf5]) if (value === null) return new Uint8Array([0xf6]) @@ -830,8 +844,8 @@ const encodeSimple = (value: boolean | null | undefined): Effect.Effect => - Effect.succeed( +const encodeFloat = (value: number, options: CodecOptions): Eff.Effect => + Eff.succeed( (() => { if (Number.isNaN(value)) { return new Uint8Array([0xf9, 0x7e, 0x00]) // Half-precision NaN @@ -868,8 +882,8 @@ const encodeFloat = (value: number, options: CodecOptions): Effect.Effect => - Effect.gen(function* () { +const decodeUint = (data: Uint8Array): Eff.Effect => + Eff.gen(function* () { const firstByte = data[0] const additionalInfo = firstByte & 0x1f @@ -914,8 +928,8 @@ const decodeUint = (data: Uint8Array): Effect.Effect => } }) -const decodeNint = (data: Uint8Array): Effect.Effect => - Effect.gen(function* () { +const decodeNint = (data: Uint8Array): Eff.Effect => + Eff.gen(function* () { const firstByte = data[0] const additionalInfo = firstByte & 0x1f @@ -960,8 +974,8 @@ const decodeNint = (data: Uint8Array): Effect.Effect => } }) -const decodeBytesWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => - Effect.gen(function* () { +const decodeBytesWithLength = (data: Uint8Array): Eff.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => + Eff.gen(function* () { const firstByte = data[0] const additionalInfo = firstByte & 0x1f @@ -1032,8 +1046,8 @@ const decodeBytesWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; by } }) -const decodeTextWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => - Effect.gen(function* () { +const decodeTextWithLength = (data: Uint8Array): Eff.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => + Eff.gen(function* () { const firstByte = data[0] const additionalInfo = firstByte & 0x1f @@ -1115,8 +1129,8 @@ const decodeTextWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; byt }) // Helper function to decode an item and return both the item and bytes consumed -const decodeItemWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => - Effect.gen(function* () { +const decodeItemWithLength = (data: Uint8Array): Eff.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => + Eff.gen(function* () { if (data.length === 0) { return yield* new CBORError({ message: "Empty CBOR data" }) } @@ -1225,8 +1239,8 @@ const decodeItemWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; byt return { item, bytesConsumed } }) -const decodeArrayWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => - Effect.gen(function* () { +const decodeArrayWithLength = (data: Uint8Array): Eff.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => + Eff.gen(function* () { const firstByte = data[0] const additionalInfo = firstByte & 0x1f @@ -1283,8 +1297,8 @@ const decodeArrayWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; by } }) -const decodeMapWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => - Effect.gen(function* () { +const decodeMapWithLength = (data: Uint8Array): Eff.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => + Eff.gen(function* () { const firstByte = data[0] const additionalInfo = firstByte & 0x1f @@ -1359,8 +1373,8 @@ const decodeMapWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; byte } }) -const decodeTagWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => - Effect.gen(function* () { +const decodeTagWithLength = (data: Uint8Array): Eff.Effect<{ item: CBOR; bytesConsumed: number }, CBORError> => + Eff.gen(function* () { const firstByte = data[0] const additionalInfo = firstByte & 0x1f let tagValue: number @@ -1428,8 +1442,8 @@ const decodeTagWithLength = (data: Uint8Array): Effect.Effect<{ item: CBOR; byte } }) -const decodeSimpleOrFloat = (data: Uint8Array): Effect.Effect => - Effect.gen(function* () { +const decodeSimpleOrFloat = (data: Uint8Array): Eff.Effect => + Eff.gen(function* () { const firstByte = data[0] const additionalInfo = firstByte & 0x1f @@ -1535,8 +1549,8 @@ const bytesToBigint = (bytes: Uint8Array): bigint => { const decodeLength = ( data: Uint8Array, offset: number -): Effect.Effect<{ length: number; bytesRead: number }, CBORError> => - Effect.gen(function* () { +): Eff.Effect<{ length: number; bytesRead: number }, CBORError> => + Eff.gen(function* () { if (offset >= data.length) { return yield* new CBORError({ message: "Insufficient data for length decoding" @@ -1634,7 +1648,7 @@ export const FromBytes = (options: CodecOptions) => Schema.transformOrFail(Schema.Uint8ArrayFromSelf, CBORValueSchema, { strict: true, decode: (fromA, _, ast) => - Effect.mapError( + Eff.mapError( internalDecode(fromA), (error) => new ParseResult.Type( @@ -1644,7 +1658,7 @@ export const FromBytes = (options: CodecOptions) => ) ), encode: (toA, _, ast) => - Effect.mapError( + Eff.mapError( internalEncode(toA, options), (error) => new ParseResult.Type( @@ -1656,12 +1670,3 @@ export const FromBytes = (options: CodecOptions) => }) export const FromHex = (options: CodecOptions) => Schema.compose(Bytes.FromHex, FromBytes(options)) - -export const Codec = (options: CodecOptions = DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - CBORError - ) diff --git a/packages/evolution/src/Certificate.ts b/packages/evolution/src/Certificate.ts index 78d69f94..be36dc77 100644 --- a/packages/evolution/src/Certificate.ts +++ b/packages/evolution/src/Certificate.ts @@ -1,12 +1,14 @@ -import { Data, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" -// import * as PoolParams from "./PoolParams.js"; // Temporarily disabled import * as Anchor from "./Anchor.js" +import * as Bytes from "./Bytes.js" +import * as CBOR from "./CBOR.js" import * as Coin from "./Coin.js" import * as Credential from "./Credential.js" import * as DRep from "./DRep.js" import * as EpochNo from "./EpochNo.js" import * as PoolKeyHash from "./PoolKeyHash.js" +import * as PoolParams from "./PoolParams.js" /** * Error class for Certificate related operations. @@ -16,7 +18,7 @@ import * as PoolKeyHash from "./PoolKeyHash.js" */ export class CertificateError extends Data.TaggedError("CertificateError")<{ message?: string - reason?: "InvalidType" | "UnsupportedCertificate" + cause?: unknown }> {} /** @@ -78,10 +80,10 @@ export const Certificate = Schema.Union( stakeCredential: Credential.Credential, poolKeyHash: PoolKeyHash.PoolKeyHash }), - // 3: pool_registration = (3, pool_params) - Temporarily disabled - // Schema.TaggedStruct("PoolRegistration", { - // poolParams: PoolParams.PoolParams, - // }), + // 3: pool_registration = (3, pool_params) + Schema.TaggedStruct("PoolRegistration", { + poolParams: PoolParams.PoolParams + }), // 4: pool_retirement = (4, pool_keyhash, epoch_no) Schema.TaggedStruct("PoolRetirement", { poolKeyHash: PoolKeyHash.PoolKeyHash, @@ -90,12 +92,12 @@ export const Certificate = Schema.Union( // 7: reg_cert = (7, stake_credential, coin) Schema.TaggedStruct("RegCert", { stakeCredential: Credential.Credential, - coin: Coin.CoinSchema + coin: Coin.Coin }), // 8: unreg_cert = (8, stake_credential, coin) Schema.TaggedStruct("UnregCert", { stakeCredential: Credential.Credential, - coin: Coin.CoinSchema + coin: Coin.Coin }), // 9: vote_deleg_cert = (9, stake_credential, drep) Schema.TaggedStruct("VoteDelegCert", { @@ -112,20 +114,20 @@ export const Certificate = Schema.Union( Schema.TaggedStruct("StakeRegDelegCert", { stakeCredential: Credential.Credential, poolKeyHash: PoolKeyHash.PoolKeyHash, - coin: Coin.CoinSchema + coin: Coin.Coin }), // 12: vote_reg_deleg_cert = (12, stake_credential, drep, coin) Schema.TaggedStruct("VoteRegDelegCert", { stakeCredential: Credential.Credential, drep: DRep.DRep, - coin: Coin.CoinSchema + coin: Coin.Coin }), // 13: stake_vote_reg_deleg_cert = (13, stake_credential, pool_keyhash, drep, coin) Schema.TaggedStruct("StakeVoteRegDelegCert", { stakeCredential: Credential.Credential, poolKeyHash: PoolKeyHash.PoolKeyHash, drep: DRep.DRep, - coin: Coin.CoinSchema + coin: Coin.Coin }), // 14: auth_committee_hot_cert = (14, committee_cold_credential, committee_hot_credential) Schema.TaggedStruct("AuthCommitteeHotCert", { @@ -140,13 +142,13 @@ export const Certificate = Schema.Union( // 16: reg_drep_cert = (16, drep_credential, coin, anchor/ nil) Schema.TaggedStruct("RegDrepCert", { drepCredential: Credential.Credential, - coin: Coin.CoinSchema, + coin: Coin.Coin, anchor: Schema.NullishOr(Anchor.Anchor) }), // 17: unreg_drep_cert = (17, drep_credential, coin) Schema.TaggedStruct("UnregDrepCert", { drepCredential: Credential.Credential, - coin: Coin.CoinSchema + coin: Coin.Coin }), // 18: update_drep_cert = (18, drep_credential, anchor/ nil) Schema.TaggedStruct("UpdateDrepCert", { @@ -155,6 +157,346 @@ export const Certificate = Schema.Union( }) ) +export const CDDLSchema = Schema.Union( + // 0: stake_registration = (0, stake_credential) + Schema.Tuple(Schema.Literal(0n), Credential.CDDLSchema), + // 1: stake_deregistration = (1, stake_credential) + Schema.Tuple(Schema.Literal(1n), Credential.CDDLSchema), + // 2: stake_delegation = (2, stake_credential, pool_keyhash) + Schema.Tuple(Schema.Literal(2n), Credential.CDDLSchema, CBOR.ByteArray), + // 3: pool_registration = (3, pool_params) + Schema.Tuple(Schema.Literal(3n), PoolParams.CDDLSchema), + // 4: pool_retirement = (4, pool_keyhash, epoch_no) + Schema.Tuple(Schema.Literal(4n), CBOR.ByteArray, CBOR.Integer), + // 7: reg_cert = (7, stake_credential , coin) + Schema.Tuple(Schema.Literal(7n), Credential.CDDLSchema, CBOR.Integer), + // 8: unreg_cert = (8, stake_credential, coin) + Schema.Tuple(Schema.Literal(8n), Credential.CDDLSchema, CBOR.Integer), + // 9: vote_deleg_cert = (9, stake_credential, drep) + Schema.Tuple( + Schema.Literal(9n), + Credential.CDDLSchema, + Schema.Union( + Schema.Tuple(Schema.Literal(0), Schema.Uint8ArrayFromSelf), + Schema.Tuple(Schema.Literal(1), Schema.Uint8ArrayFromSelf), + Schema.Tuple(Schema.Literal(2)), + Schema.Tuple(Schema.Literal(3)) + ) + ), + // 10: stake_vote_deleg_cert = (10, stake_credential, pool_keyhash, drep) + Schema.Tuple( + Schema.Literal(10n), + Credential.CDDLSchema, + CBOR.ByteArray, + Schema.Union( + Schema.Tuple(Schema.Literal(0), Schema.Uint8ArrayFromSelf), + Schema.Tuple(Schema.Literal(1), Schema.Uint8ArrayFromSelf), + Schema.Tuple(Schema.Literal(2)), + Schema.Tuple(Schema.Literal(3)) + ) + ), + // 11: stake_reg_deleg_cert = (11, stake_credential, pool_keyhash, coin) + Schema.Tuple(Schema.Literal(11n), Credential.CDDLSchema, CBOR.ByteArray, CBOR.Integer), + // 12: vote_reg_deleg_cert = (12, stake_credential, drep, coin) + Schema.Tuple(Schema.Literal(12n), Credential.CDDLSchema, DRep.CDDLSchema, CBOR.Integer), + // 13: stake_vote_reg_deleg_cert = (13, stake_credential, pool_keyhash, drep, coin) + Schema.Tuple(Schema.Literal(13n), Credential.CDDLSchema, CBOR.ByteArray, DRep.CDDLSchema, CBOR.Integer), + // 14: auth_committee_hot_cert = (14, committee_cold_credential, committee_hot_credential) + Schema.Tuple(Schema.Literal(14n), Credential.CDDLSchema, Credential.CDDLSchema), + // 15: resign_committee_cold_cert = (15, committee_cold_credential, anchor/ nil) + Schema.Tuple(Schema.Literal(15n), Credential.CDDLSchema, Schema.NullishOr(Anchor.CDDLSchema)), + // 16: reg_drep_cert = (16, drep_credential, coin, anchor/ nil) + Schema.Tuple(Schema.Literal(16n), Credential.CDDLSchema, CBOR.Integer, Schema.NullishOr(Anchor.CDDLSchema)), + // 17: unreg_drep_cert = (17, drep_credential, coin) + Schema.Tuple(Schema.Literal(17n), Credential.CDDLSchema, CBOR.Integer), + // 18: update_drep_cert = (18, drep_credential, anchor/ nil) + Schema.Tuple(Schema.Literal(18n), Credential.CDDLSchema, Schema.NullishOr(Anchor.CDDLSchema)) +) + +/** + * CDDL schema for Certificate based on Conway specification. + * + * Transforms between CBOR tuple representation and Certificate union. + * Each certificate type is encoded as [type_id, ...fields] + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Certificate), { + strict: true, + encode: (toA) => + Eff.gen(function* () { + switch (toA._tag) { + case "StakeRegistration": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + return [0n, credentialCDDL] as const + } + case "StakeDeregistration": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + return [1n, credentialCDDL] as const + } + case "StakeDelegation": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.poolKeyHash) + return [2n, credentialCDDL, poolKeyHashBytes] as const + } + case "PoolRegistration": { + const poolParamsCDDL = yield* ParseResult.encode(PoolParams.FromCDDL)(toA.poolParams) + return [3n, poolParamsCDDL] as const + } + case "PoolRetirement": { + const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.poolKeyHash) + return [4n, poolKeyHashBytes, BigInt(toA.epoch)] as const + } + case "RegCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + return [7n, credentialCDDL, BigInt(toA.coin)] as const + } + case "UnregCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + return [8n, credentialCDDL, BigInt(toA.coin)] as const + } + case "VoteDelegCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + const drepCDDL = yield* ParseResult.encode(DRep.FromCDDL)(toA.drep) + return [9n, credentialCDDL, drepCDDL] as const + } + case "StakeVoteDelegCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.poolKeyHash) + const drepCDDL = yield* ParseResult.encode(DRep.FromCDDL)(toA.drep) + return [10n, credentialCDDL, poolKeyHashBytes, drepCDDL] as const + } + case "StakeRegDelegCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.poolKeyHash) + return [11n, credentialCDDL, poolKeyHashBytes, BigInt(toA.coin)] as const + } + case "VoteRegDelegCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + const drepCDDL = yield* ParseResult.encode(DRep.FromCDDL)(toA.drep) + return [12n, credentialCDDL, drepCDDL, BigInt(toA.coin)] as const + } + case "StakeVoteRegDelegCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) + const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.poolKeyHash) + const drepCDDL = yield* ParseResult.encode(DRep.FromCDDL)(toA.drep) + return [13n, credentialCDDL, poolKeyHashBytes, drepCDDL, BigInt(toA.coin)] as const + } + case "AuthCommitteeHotCert": { + const coldCredentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.committeeColdCredential) + const hotCredentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.committeeHotCredential) + return [14n, coldCredentialCDDL, hotCredentialCDDL] as const + } + case "ResignCommitteeColdCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.committeeColdCredential) + const anchorCDDL = toA.anchor ? yield* ParseResult.encode(Anchor.FromCDDL)(toA.anchor) : null + return [15n, credentialCDDL, anchorCDDL] as const + } + case "RegDrepCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.drepCredential) + const anchorCDDL = toA.anchor ? yield* ParseResult.encode(Anchor.FromCDDL)(toA.anchor) : null + return [16n, credentialCDDL, BigInt(toA.coin), anchorCDDL] as const + } + case "UnregDrepCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.drepCredential) + return [17n, credentialCDDL, BigInt(toA.coin)] as const + } + case "UpdateDrepCert": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.drepCredential) + const anchorCDDL = toA.anchor ? yield* ParseResult.encode(Anchor.FromCDDL)(toA.anchor) : null + return [18n, credentialCDDL, anchorCDDL] as const + } + default: + return yield* ParseResult.fail( + new ParseResult.Type(CDDLSchema.ast, toA, `Unsupported certificate type: ${(toA as any)._tag}`) + ) + } + }), + decode: (fromA) => + Eff.gen(function* () { + // const [typeId, ...fields] = fromA + + switch (fromA[0]) { + case 0n: { + // stake_registration = (0, stake_credential) + // const [credentialCDDL] = fields + const [, credentialCDDL] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + return yield* ParseResult.decode(Certificate)({ _tag: "StakeRegistration", stakeCredential }) + } + case 1n: { + // stake_deregistration = (1, stake_credential) + const [, credentialCDDL] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + return yield* ParseResult.decode(Certificate)({ _tag: "StakeDeregistration", stakeCredential }) + } + case 2n: { + // stake_delegation = (2, stake_credential, pool_keyhash) + const [, credentialCDDL, poolKeyHashBytes] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const poolKeyHash = yield* ParseResult.decode(PoolKeyHash.FromBytes)(poolKeyHashBytes) + return yield* ParseResult.decode(Certificate)({ _tag: "StakeDelegation", stakeCredential, poolKeyHash }) + } + case 3n: { + // pool_registration = (3, pool_params) + const [, poolParamsCDDL] = fromA + const poolParams = yield* ParseResult.decode(PoolParams.FromCDDL)(poolParamsCDDL) + return { _tag: "PoolRegistration", poolParams } as const + } + case 4n: { + // pool_retirement = (4, pool_keyhash, epoch_no) + const [, poolKeyHashBytes, epochBigInt] = fromA + const poolKeyHash = yield* ParseResult.decode(PoolKeyHash.FromBytes)(poolKeyHashBytes) + const epoch = EpochNo.make(Number(epochBigInt)) + return yield* ParseResult.decode(Certificate)({ _tag: "PoolRetirement", poolKeyHash, epoch }) + } + case 7n: { + // reg_cert = (7, stake_credential, coin) + const [, credentialCDDL, coinBigInt] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const coin = Coin.make(coinBigInt) + return yield* ParseResult.decode(Certificate)({ _tag: "RegCert", stakeCredential, coin }) + } + case 8n: { + // unreg_cert = (8, stake_credential, coin) + const [, credentialCDDL, coinBigInt] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const coin = Coin.make(coinBigInt) + return yield* ParseResult.decode(Certificate)({ _tag: "UnregCert", stakeCredential, coin }) + } + case 9n: { + // vote_deleg_cert = (9, stake_credential, drep) + const [, credentialCDDL, drepCDDL] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const drep = yield* ParseResult.decode(DRep.FromCDDL)(drepCDDL) + return yield* ParseResult.decode(Certificate)({ _tag: "VoteDelegCert", stakeCredential, drep }) + } + case 10n: { + // stake_vote_deleg_cert = (10, stake_credential, pool_keyhash, drep) + const [, credentialCDDL, poolKeyHashBytes, drepCDDL] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const poolKeyHash = yield* ParseResult.decode(PoolKeyHash.FromBytes)(poolKeyHashBytes) + const drep = yield* ParseResult.decode(DRep.FromCDDL)(drepCDDL) + return yield* ParseResult.decode(Certificate)({ + _tag: "StakeVoteDelegCert", + stakeCredential, + poolKeyHash, + drep + }) + } + case 11n: { + // stake_reg_deleg_cert = (11, stake_credential, pool_keyhash, coin) + const [, credentialCDDL, poolKeyHashBytes, coinBigInt] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const poolKeyHash = yield* ParseResult.decode(PoolKeyHash.FromBytes)(poolKeyHashBytes) + const coin = Coin.make(coinBigInt) + return yield* ParseResult.decode(Certificate)({ + _tag: "StakeRegDelegCert", + stakeCredential, + poolKeyHash, + coin + }) + } + case 12n: { + // vote_reg_deleg_cert = (12, stake_credential, drep, coin) + const [, credentialCDDL, drepCDDL, coinBigInt] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const drep = yield* ParseResult.decode(DRep.FromCDDL)(drepCDDL) + const coin = Coin.make(coinBigInt) + return yield* ParseResult.decode(Certificate)({ _tag: "VoteRegDelegCert", stakeCredential, drep, coin }) + } + case 13n: { + // stake_vote_reg_deleg_cert = (13, stake_credential, pool_keyhash, drep, coin) + const [, credentialCDDL, poolKeyHashBytes, drepCDDL, coinBigInt] = fromA + const stakeCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const poolKeyHash = yield* ParseResult.decode(PoolKeyHash.FromBytes)(poolKeyHashBytes) + const drep = yield* ParseResult.decode(DRep.FromCDDL)(drepCDDL) + const coin = Coin.make(coinBigInt) + return yield* ParseResult.decode(Certificate)({ + _tag: "StakeVoteRegDelegCert", + stakeCredential, + poolKeyHash, + drep, + coin + }) + } + case 14n: { + // auth_committee_hot_cert = (14, committee_cold_credential, committee_hot_credential) + const [, coldCredentialCDDL, hotCredentialCDDL] = fromA + const committeeColdCredential = yield* ParseResult.decode(Credential.FromCDDL)(coldCredentialCDDL) + const committeeHotCredential = yield* ParseResult.decode(Credential.FromCDDL)(hotCredentialCDDL) + return yield* ParseResult.decode(Certificate)({ + _tag: "AuthCommitteeHotCert", + committeeColdCredential, + committeeHotCredential + }) + } + case 15n: { + // resign_committee_cold_cert = (15, committee_cold_credential, anchor/ nil) + const [, credentialCDDL, anchorCDDL] = fromA + const committeeColdCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const anchor = anchorCDDL ? yield* ParseResult.decode(Anchor.FromCDDL)(anchorCDDL) : undefined + return yield* ParseResult.decode(Certificate)({ + _tag: "ResignCommitteeColdCert", + committeeColdCredential, + anchor + }) + } + case 16n: { + // reg_drep_cert = (16, drep_credential, coin, anchor/ nil) + const [, credentialCDDL, coinBigInt, anchorCDDL] = fromA + const drepCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const coin = Coin.make(coinBigInt) + const anchor = anchorCDDL ? yield* ParseResult.decode(Anchor.FromCDDL)(anchorCDDL) : undefined + return yield* ParseResult.decode(Certificate)({ _tag: "RegDrepCert", drepCredential, coin, anchor }) + } + case 17n: { + // unreg_drep_cert = (17, drep_credential, coin) + const [, credentialCDDL, coinBigInt] = fromA + const drepCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const coin = Coin.make(coinBigInt) + return yield* ParseResult.decode(Certificate)({ _tag: "UnregDrepCert", drepCredential, coin }) + } + case 18n: { + // update_drep_cert = (18, drep_credential, anchor/ nil) + const [, credentialCDDL, anchorCDDL] = fromA + const drepCredential = yield* ParseResult.decode(Credential.FromCDDL)(credentialCDDL) + const anchor = anchorCDDL ? yield* ParseResult.decode(Anchor.FromCDDL)(anchorCDDL) : undefined + return yield* ParseResult.decode(Certificate)({ _tag: "UpdateDrepCert", drepCredential, anchor }) + } + default: + return yield* ParseResult.fail( + new ParseResult.Type(CDDLSchema.ast, fromA, `Unsupported certificate type ID: ${fromA}`) + ) + } + }) +}) + +/** + * CBOR bytes transformation schema for Certificate. + * + * @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 → Certificate + ) + +/** + * CBOR hex transformation schema for Certificate. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose( + Bytes.FromHex, // string → Uint8Array + FromCBORBytes(options) // Uint8Array → Certificate + ) + /** * Type alias for Certificate. * @@ -162,3 +504,243 @@ export const Certificate = Schema.Union( * @category model */ export type Certificate = typeof Certificate.Type + +/** + * Check if the given value is a valid Certificate. + * + * @since 2.0.0 + * @category predicates + */ +export const is = Schema.is(Certificate) + +/** + * FastCheck arbitrary for Certificate instances. + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary = FastCheck.oneof( + // StakeRegistration + FastCheck.record({ + _tag: FastCheck.constant("StakeRegistration"), + stakeCredential: Credential.arbitrary + }), + // StakeDeregistration + FastCheck.record({ + _tag: FastCheck.constant("StakeDeregistration"), + stakeCredential: Credential.arbitrary + }), + // StakeDelegation + FastCheck.record({ + _tag: FastCheck.constant("StakeDelegation"), + stakeCredential: Credential.arbitrary, + poolKeyHash: PoolKeyHash.arbitrary + }), + // PoolRetirement + FastCheck.record({ + _tag: FastCheck.constant("PoolRetirement"), + poolKeyHash: PoolKeyHash.arbitrary, + epoch: EpochNo.generator + }), + // RegCert + FastCheck.record({ + _tag: FastCheck.constant("RegCert"), + stakeCredential: Credential.arbitrary, + coin: Coin.arbitrary + }), + // UnregCert + FastCheck.record({ + _tag: FastCheck.constant("UnregCert"), + stakeCredential: Credential.arbitrary, + coin: Coin.arbitrary + }), + // VoteDelegCert + FastCheck.record({ + _tag: FastCheck.constant("VoteDelegCert"), + stakeCredential: Credential.arbitrary, + drep: DRep.arbitrary + }) + // Note: Additional certificate types would be added here +) + +/** + * Check if two Certificate instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: Certificate, b: Certificate): boolean => { + if (a._tag !== b._tag) return false + + switch (a._tag) { + case "StakeRegistration": + return b._tag === "StakeRegistration" && Credential.equals(a.stakeCredential, b.stakeCredential) + case "StakeDeregistration": + return b._tag === "StakeDeregistration" && Credential.equals(a.stakeCredential, b.stakeCredential) + case "StakeDelegation": + return ( + b._tag === "StakeDelegation" && + Credential.equals(a.stakeCredential, b.stakeCredential) && + PoolKeyHash.equals(a.poolKeyHash, b.poolKeyHash) + ) + case "PoolRetirement": + return ( + b._tag === "PoolRetirement" && + PoolKeyHash.equals(a.poolKeyHash, b.poolKeyHash) && + EpochNo.equals(a.epoch, b.epoch) + ) + case "RegCert": + return ( + b._tag === "RegCert" && Credential.equals(a.stakeCredential, b.stakeCredential) && Coin.equals(a.coin, b.coin) + ) + case "UnregCert": + return ( + b._tag === "UnregCert" && Credential.equals(a.stakeCredential, b.stakeCredential) && Coin.equals(a.coin, b.coin) + ) + case "VoteDelegCert": + return ( + b._tag === "VoteDelegCert" && + Credential.equals(a.stakeCredential, b.stakeCredential) && + DRep.equals(a.drep, b.drep) + ) + // Add other cases as needed + default: + return false + } +} + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse a Certificate from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Certificate => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse a Certificate from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Certificate => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert a Certificate to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (certificate: Certificate, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(certificate, options)) + +/** + * Convert a Certificate to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (certificate: Certificate, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(certificate, options)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse a Certificate from CBOR bytes. + * + * @since 2.0.0 + * @category effect + */ + export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (error) => + new CertificateError({ + message: "Failed to decode Certificate from CBOR bytes", + cause: error + }) + ) + ) + + /** + * Parse a Certificate from CBOR hex string. + * + * @since 2.0.0 + * @category effect + */ + export const fromCBORHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (error) => + new CertificateError({ + message: "Failed to decode Certificate from CBOR hex", + cause: error + }) + ) + ) + + /** + * Convert a Certificate to CBOR bytes. + * + * @since 2.0.0 + * @category effect + */ + export const toCBORBytes = ( + certificate: Certificate, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORBytes(options))(certificate).pipe( + Eff.mapError( + (error) => + new CertificateError({ + message: "Failed to encode Certificate to CBOR bytes", + cause: error + }) + ) + ) + + /** + * Convert a Certificate to CBOR hex string. + * + * @since 2.0.0 + * @category effect + */ + export const toCBORHex = ( + certificate: Certificate, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORHex(options))(certificate).pipe( + Eff.mapError( + (error) => + new CertificateError({ + message: "Failed to encode Certificate to CBOR hex", + cause: error + }) + ) + ) +} diff --git a/packages/evolution/src/Coin.ts b/packages/evolution/src/Coin.ts index f2b092e8..e7d9a2ac 100644 --- a/packages/evolution/src/Coin.ts +++ b/packages/evolution/src/Coin.ts @@ -1,4 +1,4 @@ -import { Data, FastCheck, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" /** * Error class for Coin related operations. @@ -26,8 +26,8 @@ export const MAX_COIN_VALUE = 18446744073709551615n * @since 2.0.0 * @category schemas */ -export const CoinSchema = Schema.BigIntFromSelf.pipe( - Schema.filter((value) => value >= 0n && value <= MAX_COIN_VALUE) +export const Coin = Schema.BigIntFromSelf.pipe( + Schema.filter((value) => value >= 0n && value <= MAX_COIN_VALUE), ).annotations({ message: (issue) => `Coin must be between 0 and ${MAX_COIN_VALUE}, but got ${issue.actual}`, identifier: "Coin" @@ -40,7 +40,7 @@ export const CoinSchema = Schema.BigIntFromSelf.pipe( * @since 2.0.0 * @category model */ -export type Coin = typeof CoinSchema.Type +export type Coin = typeof Coin.Type /** * Smart constructor for creating Coin values. @@ -48,7 +48,7 @@ export type Coin = typeof CoinSchema.Type * @since 2.0.0 * @category constructors */ -export const make = CoinSchema.make +export const make = Coin.make /** * Check if a value is a valid Coin. @@ -56,7 +56,7 @@ export const make = CoinSchema.make * @since 2.0.0 * @category predicates */ -export const is = Schema.is(CoinSchema) +export const is = Schema.is(Coin) /** * Add two coin amounts safely. @@ -64,15 +64,7 @@ export const is = Schema.is(CoinSchema) * @since 2.0.0 * @category transformation */ -export const add = (a: Coin, b: Coin): Coin => { - const result = a + b - if (result > MAX_COIN_VALUE) { - throw new CoinError({ - message: `Addition overflow: ${a} + ${b} exceeds maximum coin value` - }) - } - return result -} +export const add = (a: Coin, b: Coin): Coin => Eff.runSync(Effect.add(a, b)) /** * Subtract two coin amounts safely. @@ -80,15 +72,7 @@ export const add = (a: Coin, b: Coin): Coin => { * @since 2.0.0 * @category transformation */ -export const subtract = (a: Coin, b: Coin): Coin => { - const result = a - b - if (result < 0n) { - throw new CoinError({ - message: `Subtraction underflow: ${a} - ${b} results in negative value` - }) - } - return result -} +export const subtract = (a: Coin, b: Coin): Coin => Eff.runSync(Effect.subtract(a, b)) /** * Compare two coin amounts. @@ -116,7 +100,55 @@ export const equals = (a: Coin, b: Coin): boolean => a === b * @since 2.0.0 * @category generators */ -export const generator = FastCheck.bigInt({ +export const arbitrary = FastCheck.bigInt({ min: 0n, max: MAX_COIN_VALUE -}) +}).map(make) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Add two coin amounts safely with Effect error handling. + * + * @since 2.0.0 + * @category transformation + */ + export const add = (a: Coin, b: Coin): Eff.Effect => { + const result = a + b + if (result > MAX_COIN_VALUE) { + return Eff.fail( + new CoinError({ + message: `Addition overflow: ${a} + ${b} exceeds maximum coin value` + }) + ) + } + return Eff.succeed(make(result)) + } + + /** + * Subtract two coin amounts safely with Effect error handling. + * + * @since 2.0.0 + * @category transformation + */ + export const subtract = (a: Coin, b: Coin): Eff.Effect => { + const result = a - b + if (result < 0n) { + return Eff.fail( + new CoinError({ + message: `Subtraction underflow: ${a} - ${b} results in negative value` + }) + ) + } + return Eff.succeed(make(result)) + } +} diff --git a/packages/evolution/src/CommitteeColdCredential.ts b/packages/evolution/src/CommitteeColdCredential.ts index 5dc1a00d..637810c2 100644 --- a/packages/evolution/src/CommitteeColdCredential.ts +++ b/packages/evolution/src/CommitteeColdCredential.ts @@ -7,49 +7,6 @@ * @since 2.0.0 */ -import type * as CBOR from "./CBOR.js" import * as Credential from "./Credential.js" -/** - * Error class for CommitteeColdCredential operations - re-exports CredentialError. - * - * @since 2.0.0 - * @category errors - */ -export const CommitteeColdCredentialError = Credential.CredentialError - -/** - * CommitteeColdCredential schema - alias for the Credential schema. - * committee_cold_credential = credential - * - * @since 2.0.0 - * @category schemas - */ -export const CommitteeColdCredential = Credential.Credential - -/** - * Type representing a committee cold credential - alias for Credential type. - * - * @since 2.0.0 - * @category model - */ -export type CommitteeColdCredential = Credential.Credential - -/** - * Re-exported utilities from Credential module. - * - * @since 2.0.0 - */ -export const is = Credential.is -export const equals = Credential.equals -export const generator = Credential.generator -export const Codec = (options?: CBOR.CodecOptions) => Credential.Codec(options) - -/** - * CBOR encoding/decoding schemas. - * - * @since 2.0.0 - * @category schemas - */ -export const FromCBORBytes = Credential.FromCBORBytes -export const FromCBORHex = Credential.FromCBORHex +export const CommitteeColdCredential = Credential diff --git a/packages/evolution/src/CommitteeHotCredential.ts b/packages/evolution/src/CommitteeHotCredential.ts index d7cb6c28..981a698e 100644 --- a/packages/evolution/src/CommitteeHotCredential.ts +++ b/packages/evolution/src/CommitteeHotCredential.ts @@ -9,46 +9,4 @@ import * as Credential from "./Credential.js" -/** - * Error class for CommitteeHotCredential operations - re-exports CredentialError. - * - * @since 2.0.0 - * @category errors - */ -export const CommitteeHotCredentialError = Credential.CredentialError - -/** - * CommitteeHotCredential schema - alias for the Credential schema. - * committee_hot_credential = credential - * - * @since 2.0.0 - * @category schemas - */ -export const CommitteeHotCredential = Credential.Credential - -/** - * Type representing a committee hot credential - alias for Credential type. - * - * @since 2.0.0 - * @category model - */ -export type CommitteeHotCredential = Credential.Credential - -/** - * Re-exported utilities from Credential module. - * - * @since 2.0.0 - */ -export const is = Credential.is -export const equals = Credential.equals -export const generator = Credential.generator -export const Codec = Credential.Codec - -/** - * CBOR encoding/decoding schemas. - * - * @since 2.0.0 - * @category schemas - */ -export const FromCBORBytes = Credential.FromCBORBytes -export const FromCBORHex = Credential.FromCBORHex +export const CommitteeHotCredential = Credential diff --git a/packages/evolution/src/Credential.ts b/packages/evolution/src/Credential.ts index e5bc98ca..d0382457 100644 --- a/packages/evolution/src/Credential.ts +++ b/packages/evolution/src/Credential.ts @@ -1,8 +1,7 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as KeyHash from "./KeyHash.js" import * as ScriptHash from "./ScriptHash.js" @@ -52,7 +51,16 @@ export type Credential = typeof Credential.Type */ export const is = Schema.is(Credential) -export const CDDL = Schema.Tuple( +/** + * Smart constructors for Credential variants. + * + * @since 2.0.0 + * @category constructors + */ +export const makeKeyHash = (hash: KeyHash.KeyHash): Credential => ({ _tag: "KeyHash", hash }) +export const makeScriptHash = (hash: ScriptHash.ScriptHash): Credential => ({ _tag: "ScriptHash", hash }) + +export const CDDLSchema = Schema.Tuple( Schema.Literal(0n, 1n), Schema.Uint8ArrayFromSelf // hash bytes ) @@ -64,57 +72,48 @@ export const CDDL = Schema.Tuple( * @since 2.0.0 * @category schemas */ -export const FromCDDL = Schema.transformOrFail(CDDL, Schema.typeSchema(Credential), { +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Credential), { strict: true, encode: (toI) => - Effect.gen(function* () { + Eff.gen(function* () { switch (toI._tag) { case "KeyHash": { const keyHashBytes = yield* ParseResult.encode(KeyHash.FromBytes)(toI.hash) return [0n, keyHashBytes] as const } case "ScriptHash": { - const scriptHashBytes = yield* ParseResult.encode(ScriptHash.BytesSchema)(toI.hash) + const scriptHashBytes = yield* ParseResult.encode(ScriptHash.FromBytes)(toI.hash) return [1n, scriptHashBytes] as const } } }), decode: ([tag, hashBytes]) => - Effect.gen(function* () { + Eff.gen(function* () { switch (tag) { case 0n: { const keyHash = yield* ParseResult.decode(KeyHash.FromBytes)(hashBytes) return Credential.members[0].make({ hash: keyHash }) } case 1n: { - const scriptHash = yield* ParseResult.decode(ScriptHash.BytesSchema)(hashBytes) + const scriptHash = yield* ParseResult.decode(ScriptHash.FromBytes)(hashBytes) return Credential.members[1].make({ hash: scriptHash }) } } }) }) -export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → Credential ) -export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array FromCBORBytes(options) // Uint8Array → Credential ) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromCBORBytes(options), - cborHex: FromCBORHex(options) - }, - CredentialError - ) - /** * Check if two Credential instances are equal. * @@ -126,19 +125,119 @@ export const equals = (a: Credential, b: Credential): boolean => { } /** - * Generate a random Credential. + * FastCheck arbitrary for generating random Credential instances. * Randomly selects between generating a KeyHash or ScriptHash credential. * * @since 2.0.0 - * @category generators + * @category testing */ -export const generator = FastCheck.oneof( +export const arbitrary = FastCheck.oneof( FastCheck.record({ _tag: FastCheck.constant("KeyHash" as const), - hash: KeyHash.generator + hash: KeyHash.arbitrary }), FastCheck.record({ _tag: FastCheck.constant("ScriptHash" as const), - hash: ScriptHash.generator + hash: ScriptHash.arbitrary }) ) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse a Credential from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Credential => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse a Credential from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Credential => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Convert a Credential to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (credential: Credential, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(credential, options)) + +/** + * Convert a Credential to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (credential: Credential, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(credential, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse a Credential from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (error) => new CredentialError({ message: "Failed to decode Credential from CBOR bytes", cause: error }) + ) + + /** + * Parse a Credential from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (error) => new CredentialError({ message: "Failed to decode Credential from CBOR hex", cause: error }) + ) + + /** + * Convert a Credential to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (credential: Credential, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(credential), + (error) => new CredentialError({ message: "Failed to encode Credential to CBOR bytes", cause: error }) + ) + + /** + * Convert a Credential to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (credential: Credential, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(credential), + (error) => new CredentialError({ message: "Failed to encode Credential to CBOR hex", cause: error }) + ) +} diff --git a/packages/evolution/src/DRep.ts b/packages/evolution/src/DRep.ts index 403ad749..60661205 100644 --- a/packages/evolution/src/DRep.ts +++ b/packages/evolution/src/DRep.ts @@ -1,8 +1,7 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as KeyHash from "./KeyHash.js" import * as ScriptHash from "./ScriptHash.js" @@ -14,7 +13,7 @@ import * as ScriptHash from "./ScriptHash.js" */ export class DRepError extends Data.TaggedError("DRepError")<{ message?: string - reason?: "InvalidStructure" | "UnsupportedType" + cause?: unknown }> {} /** @@ -44,6 +43,13 @@ export const DRep = Schema.Union( */ export type DRep = typeof DRep.Type +export const CDDLSchema = Schema.Union( + Schema.Tuple(Schema.Literal(0), Schema.Uint8ArrayFromSelf), + Schema.Tuple(Schema.Literal(1), Schema.Uint8ArrayFromSelf), + Schema.Tuple(Schema.Literal(2)), + Schema.Tuple(Schema.Literal(3)) +) + /** * CDDL schema for DRep with proper transformation. * drep = [0, addr_keyhash] / [1, script_hash] / [2] / [3] @@ -51,67 +57,58 @@ export type DRep = typeof DRep.Type * @since 2.0.0 * @category schemas */ -export const DRepCDDLSchema = Schema.transformOrFail( - Schema.Union( - Schema.Tuple(Schema.Literal(0), Schema.Uint8ArrayFromSelf), - Schema.Tuple(Schema.Literal(1), Schema.Uint8ArrayFromSelf), - Schema.Tuple(Schema.Literal(2)), - Schema.Tuple(Schema.Literal(3)) - ), - Schema.typeSchema(DRep), - { - strict: true, - encode: (toA) => - Effect.gen(function* () { - switch (toA._tag) { - case "KeyHashDRep": { - const keyHashBytes = yield* ParseResult.encode(KeyHash.FromBytes)(toA.keyHash) - return [0, keyHashBytes] as const - } - case "ScriptHashDRep": { - const scriptHashBytes = yield* ParseResult.encode(ScriptHash.BytesSchema)(toA.scriptHash) - return [1, scriptHashBytes] as const - } - case "AlwaysAbstainDRep": - return [2] as const - case "AlwaysNoConfidenceDRep": - return [3] as const +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(DRep), { + strict: true, + encode: (toA) => + Eff.gen(function* () { + switch (toA._tag) { + case "KeyHashDRep": { + const keyHashBytes = yield* ParseResult.encode(KeyHash.FromBytes)(toA.keyHash) + return [0, keyHashBytes] as const } - }), - decode: (fromA) => - Effect.gen(function* () { - const [tag, ...rest] = fromA - switch (tag) { - case 0: { - const keyHash = yield* ParseResult.decode(KeyHash.FromBytes)(rest[0] as Uint8Array) - return yield* ParseResult.decode(DRep)({ - _tag: "KeyHashDRep", - keyHash - }) - } - case 1: { - const scriptHash = yield* ParseResult.decode(ScriptHash.BytesSchema)(rest[0] as Uint8Array) - return yield* ParseResult.decode(DRep)({ - _tag: "ScriptHashDRep", - scriptHash - }) - } - case 2: - return yield* ParseResult.decode(DRep)({ - _tag: "AlwaysAbstainDRep" - }) - case 3: - return yield* ParseResult.decode(DRep)({ - _tag: "AlwaysNoConfidenceDRep" - }) - default: - return yield* ParseResult.fail( - new ParseResult.Type(Schema.typeSchema(DRep).ast, fromA, `Invalid DRep tag: ${tag}`) - ) + case "ScriptHashDRep": { + const scriptHashBytes = yield* ParseResult.encode(ScriptHash.FromBytes)(toA.scriptHash) + return [1, scriptHashBytes] as const } - }) - } -) + case "AlwaysAbstainDRep": + return [2] as const + case "AlwaysNoConfidenceDRep": + return [3] as const + } + }), + decode: (fromA) => + Eff.gen(function* () { + const [tag, ...rest] = fromA + switch (tag) { + case 0: { + const keyHash = yield* ParseResult.decode(KeyHash.FromBytes)(rest[0] as Uint8Array) + return yield* ParseResult.decode(DRep)({ + _tag: "KeyHashDRep", + keyHash + }) + } + case 1: { + const scriptHash = yield* ParseResult.decode(ScriptHash.FromBytes)(rest[0] as Uint8Array) + return yield* ParseResult.decode(DRep)({ + _tag: "ScriptHashDRep", + scriptHash + }) + } + case 2: + return yield* ParseResult.decode(DRep)({ + _tag: "AlwaysAbstainDRep" + }) + case 3: + return yield* ParseResult.decode(DRep)({ + _tag: "AlwaysNoConfidenceDRep" + }) + default: + return yield* ParseResult.fail( + new ParseResult.Type(Schema.typeSchema(DRep).ast, fromA, `Invalid DRep tag: ${tag}`) + ) + } + }) +}) /** * Type alias for KeyHashDRep. @@ -151,10 +148,10 @@ export type AlwaysNoConfidenceDRep = Extract +export const FromBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - DRepCDDLSchema // CBOR → DRep + FromCDDL // CBOR → DRep ) /** @@ -163,107 +160,163 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array FromBytes(options) // Uint8Array → DRep ) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - DRepError - ) - /** - * Pattern match on a DRep to handle different DRep types. + * Check if the given value is a valid DRep * * @since 2.0.0 - * @category transformation + * @category predicates */ -export const match = ( - drep: DRep, - cases: { - KeyHashDRep: (drep: KeyHashDRep) => A - ScriptHashDRep: (drep: ScriptHashDRep) => B - AlwaysAbstainDRep: (drep: AlwaysAbstainDRep) => C - AlwaysNoConfidenceDRep: (drep: AlwaysNoConfidenceDRep) => D - } -): A | B | C | D => { - switch (drep._tag) { - case "KeyHashDRep": - return cases.KeyHashDRep(drep) - case "ScriptHashDRep": - return cases.ScriptHashDRep(drep) - case "AlwaysAbstainDRep": - return cases.AlwaysAbstainDRep(drep) - case "AlwaysNoConfidenceDRep": - return cases.AlwaysNoConfidenceDRep(drep) - default: - throw new Error(`Exhaustive check failed: Unhandled case '${(drep as { _tag: string })._tag}' encountered.`) - } -} +export const isDRep = Schema.is(DRep) /** - * Check if a DRep is a KeyHashDRep. + * FastCheck arbitrary for generating random DRep instances. * * @since 2.0.0 - * @category predicates + * @category arbitrary */ -export const isKeyHashDRep = (drep: DRep): drep is KeyHashDRep => drep._tag === "KeyHashDRep" +export const arbitrary = FastCheck.oneof( + FastCheck.record({ + keyHash: KeyHash.arbitrary + }).map((props) => ({ _tag: "KeyHashDRep" as const, ...props })), + FastCheck.record({ + scriptHash: ScriptHash.arbitrary + }).map((props) => ({ _tag: "ScriptHashDRep" as const, ...props })), + FastCheck.record({}).map(() => ({ _tag: "AlwaysAbstainDRep" as const })), + FastCheck.record({}).map(() => ({ _tag: "AlwaysNoConfidenceDRep" as const })) +) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Check if a DRep is a ScriptHashDRep. + * Parse DRep from CBOR bytes. * * @since 2.0.0 - * @category predicates + * @category parsing */ -export const isScriptHashDRep = (drep: DRep): drep is ScriptHashDRep => drep._tag === "ScriptHashDRep" +export const fromBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): DRep => + Eff.runSync(Effect.fromBytes(bytes, options)) /** - * Check if a DRep is an AlwaysAbstainDRep. + * Parse DRep from CBOR hex string. * * @since 2.0.0 - * @category predicates + * @category parsing */ -export const isAlwaysAbstainDRep = (drep: DRep): drep is AlwaysAbstainDRep => drep._tag === "AlwaysAbstainDRep" +export const fromHex = (hex: string, options?: CBOR.CodecOptions): DRep => Eff.runSync(Effect.fromHex(hex, options)) /** - * Check if a DRep is an AlwaysNoConfidenceDRep. + * Encode DRep to CBOR bytes. * * @since 2.0.0 - * @category predicates + * @category encoding */ -export const isAlwaysNoConfidenceDRep = (drep: DRep): drep is AlwaysNoConfidenceDRep => - drep._tag === "AlwaysNoConfidenceDRep" +export const toBytes = (drep: DRep, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toBytes(drep, options)) /** - * Check if the given value is a valid DRep + * Encode DRep to CBOR hex string. * * @since 2.0.0 - * @category predicates + * @category encoding */ -export const isDRep = Schema.is(DRep) +export const toHex = (drep: DRep, options?: CBOR.CodecOptions): string => Eff.runSync(Effect.toHex(drep, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ /** - * FastCheck generator for DRep instances. + * Effect-based error handling variants for functions that can fail. * * @since 2.0.0 - * @category generators + * @category effect */ -export const generator = FastCheck.oneof( - FastCheck.record({ - keyHash: KeyHash.generator - }).map((props) => ({ _tag: "KeyHashDRep" as const, ...props })), - FastCheck.record({ - scriptHash: ScriptHash.generator - }).map((props) => ({ _tag: "ScriptHashDRep" as const, ...props })), - FastCheck.record({}).map(() => ({ _tag: "AlwaysAbstainDRep" as const })), - FastCheck.record({}).map(() => ({ _tag: "AlwaysNoConfidenceDRep" as const })) -) +export namespace Effect { + /** + * Parse DRep from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new DRepError({ + message: "Failed to parse DRep from bytes", + cause + }) + ) + ) + + /** + * Parse DRep from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new DRepError({ + message: "Failed to parse DRep from hex", + cause + }) + ) + ) + + /** + * Encode DRep to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = ( + drep: DRep, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromBytes(options))(drep).pipe( + Eff.mapError( + (cause) => + new DRepError({ + message: "Failed to encode DRep to bytes", + cause + }) + ) + ) + + /** + * Encode DRep to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (drep: DRep, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + Schema.encode(FromHex(options))(drep).pipe( + Eff.mapError( + (cause) => + new DRepError({ + message: "Failed to encode DRep to hex", + cause + }) + ) + ) +} /** * Check if two DRep instances are equal. @@ -328,3 +381,62 @@ export const alwaysAbstain = (): AlwaysAbstainDRep => ({ export const alwaysNoConfidence = (): AlwaysNoConfidenceDRep => ({ _tag: "AlwaysNoConfidenceDRep" }) + +/** + * Pattern match over DRep. + * + * @since 2.0.0 + * @category pattern matching + */ +export const match = + (patterns: { + KeyHashDRep: (keyHash: KeyHash.KeyHash) => A + ScriptHashDRep: (scriptHash: ScriptHash.ScriptHash) => A + AlwaysAbstainDRep: () => A + AlwaysNoConfidenceDRep: () => A + }) => + (drep: DRep) => { + switch (drep._tag) { + case "KeyHashDRep": + return patterns.KeyHashDRep(drep.keyHash) + case "ScriptHashDRep": + return patterns.ScriptHashDRep(drep.scriptHash) + case "AlwaysAbstainDRep": + return patterns.AlwaysAbstainDRep() + case "AlwaysNoConfidenceDRep": + return patterns.AlwaysNoConfidenceDRep() + } + } + +/** + * Check if DRep is a KeyHashDRep. + * + * @since 2.0.0 + * @category type guards + */ +export const isKeyHashDRep = (drep: DRep): drep is KeyHashDRep => drep._tag === "KeyHashDRep" + +/** + * Check if DRep is a ScriptHashDRep. + * + * @since 2.0.0 + * @category type guards + */ +export const isScriptHashDRep = (drep: DRep): drep is ScriptHashDRep => drep._tag === "ScriptHashDRep" + +/** + * Check if DRep is an AlwaysAbstainDRep. + * + * @since 2.0.0 + * @category type guards + */ +export const isAlwaysAbstainDRep = (drep: DRep): drep is AlwaysAbstainDRep => drep._tag === "AlwaysAbstainDRep" + +/** + * Check if DRep is an AlwaysNoConfidenceDRep. + * + * @since 2.0.0 + * @category type guards + */ +export const isAlwaysNoConfidenceDRep = (drep: DRep): drep is AlwaysNoConfidenceDRep => + drep._tag === "AlwaysNoConfidenceDRep" diff --git a/packages/evolution/src/DRepCredential.ts b/packages/evolution/src/DRepCredential.ts index 3835a28e..8bff6165 100644 --- a/packages/evolution/src/DRepCredential.ts +++ b/packages/evolution/src/DRepCredential.ts @@ -9,46 +9,4 @@ import * as Credential from "./Credential.js" -/** - * Error class for DRepCredential operations - re-exports CredentialError. - * - * @since 2.0.0 - * @category errors - */ -export const DRepCredentialError = Credential.CredentialError - -/** - * DRepCredential schema - alias for the Credential schema. - * drep_credential = credential - * - * @since 2.0.0 - * @category schemas - */ -export const DRepCredential = Credential.Credential - -/** - * Type representing a DRep credential - alias for Credential type. - * - * @since 2.0.0 - * @category model - */ -export type DRepCredential = Credential.Credential - -/** - * Re-exported utilities from Credential module. - * - * @since 2.0.0 - */ -export const isCredential = Credential.is -export const equals = Credential.equals -export const generator = Credential.generator -export const Codec = Credential.Codec - -/** - * CBOR encoding/decoding schemas. - * - * @since 2.0.0 - * @category schemas - */ -export const FromBytes = Credential.FromCBORBytes -export const FromHex = Credential.FromCBORHex +export const DRepCredential = Credential diff --git a/packages/evolution/src/Data.ts b/packages/evolution/src/Data.ts index 0613c3b6..91679466 100644 --- a/packages/evolution/src/Data.ts +++ b/packages/evolution/src/Data.ts @@ -1,8 +1,7 @@ -import { Data as EffectData, Effect, FastCheck, ParseResult, pipe, Schema } from "effect" +import { Data as EffectData, Either as E, FastCheck, ParseResult, pipe, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as Numeric from "./Numeric.js" /** @@ -47,7 +46,7 @@ export class DataError extends EffectData.TaggedError("DataError")<{ * @since 2.0.0 * @category model */ -export type Data = Constr | MapList | List | Int | ByteArray +export type Data = Constr | Map | List | Int | ByteArray /** * Constr type for constructor alternatives based on Conway CDDL specification @@ -77,7 +76,7 @@ export type Data = Constr | MapList | List | Int | ByteArray // readonly fields: readonly Data[]; // } -export type MapList = Map +export type Map = globalThis.Map /** * PlutusList type for plutus data lists @@ -99,8 +98,16 @@ export type List = ReadonlyArray // fields: Schema.Array(Schema.suspend((): Schema.Schema => DataSchema)), // }); export class Constr extends Schema.Class("Constr")({ - index: Numeric.Uint64Schema, - fields: Schema.Array(Schema.suspend((): Schema.Schema => DataSchema)) + index: Numeric.Uint64Schema.annotations({ + identifier: "Data.Constr.Index", + title: "Constructor Index", + description: "The index of the constructor, must be a non-negative integer" + }), + fields: Schema.Array(Schema.suspend((): Schema.Schema => DataSchema)).annotations({ + identifier: "Data.Constr.Fields", + title: "Fields of Constr", + description: "A list of PlutusData fields for the constructor" + }) }) {} /** @@ -111,10 +118,20 @@ export class Constr extends Schema.Class("Constr")({ * @since 2.0.0 */ export const MapSchema = Schema.MapFromSelf({ - key: Schema.suspend((): Schema.Schema => DataSchema), - value: Schema.suspend((): Schema.Schema => DataSchema) + key: Schema.suspend((): Schema.Schema => DataSchema).annotations({ + identifier: "Data.Map.Key", + title: "Map Key", + description: "The key of the PlutusMap, must be a PlutusData type" + }), + value: Schema.suspend((): Schema.Schema => DataSchema).annotations({ + identifier: "Data.Map.Value", + title: "Map Value", + description: "The value of the PlutusMap, must be a PlutusData type" + }) }).annotations({ - identifier: "Data.Map" + identifier: "Data.Map", + title: "PlutusMap", + description: "A map of PlutusData key-value pairs" }) /** @@ -124,7 +141,9 @@ export const MapSchema = Schema.MapFromSelf({ * * @since 2.0.0 */ -export const ListSchema = Schema.Array(Schema.suspend((): Schema.Schema => DataSchema)) +export const ListSchema = Schema.Array(Schema.suspend((): Schema.Schema => DataSchema)).annotations({ + identifier: "Data.List" +}) /** * Schema for PlutusBigInt data type @@ -159,10 +178,10 @@ export type Int = typeof IntSchema.Type * * @since 2.0.0 */ -export const BytesSchema = Bytes.HexLenientSchema.annotations({ - identifier: "Data.Bytes" +export const ByteArray = Bytes.HexLenientSchema.annotations({ + identifier: "Data.ByteArray" }) -export type ByteArray = typeof BytesSchema.Type +export type ByteArray = typeof ByteArray.Type /** * Combined schema for PlutusData type @@ -176,7 +195,7 @@ export const DataSchema: Schema.Schema = Schema.Union( Schema.typeSchema(MapSchema), ListSchema, Schema.typeSchema(IntSchema), - BytesSchema + ByteArray ).annotations({ identifier: "Data" }) @@ -227,7 +246,7 @@ export const isInt = Schema.is(IntSchema) * * @since 2.0.0 */ -export const isBytes = Schema.is(BytesSchema) +export const isBytes = Schema.is(ByteArray) /** * Creates a constructor with the specified index and data @@ -235,11 +254,12 @@ export const isBytes = Schema.is(BytesSchema) * @since 2.0.0 * @category constructors */ -export const constr = (index: bigint, data: Array): Constr => - new Constr({ - index, - fields: data - }) +export const constr = (index: bigint, fields: Array): Constr => Schema.decodeSync(Constr)({ index, fields }) + +// new Constr({ +// index: Numeric.Uint64Make(index), +// fields: data +// }) /** * Creates a Plutus map from key-value pairs @@ -247,8 +267,8 @@ export const constr = (index: bigint, data: Array): Constr => * @since 2.0.0 * @category constructors */ -export const map = (entries: Array<{ key: Data; value: Data }>): MapList => - new Map(entries.map(({ key, value }) => [key, value])) +export const map = (entries: Array<[key: Data, value: Data]>) => + Schema.decodeSync(MapSchema)(new globalThis.Map(entries)) /** * Creates a Plutus list from items @@ -256,7 +276,7 @@ export const map = (entries: Array<{ key: Data; value: Data }>): MapList => * @since 2.0.0 * @category constructors */ -export const list = (list: Array): List => list +export const list = (list: Array): List => Schema.decodeSync(ListSchema)(list) /** * Creates Plutus big integer @@ -264,7 +284,7 @@ export const list = (list: Array): List => list * @since 2.0.0 * @category constructors */ -export const int = (integer: bigint): Int => integer +export const int = (integer: bigint): Int => Schema.decodeSync(IntSchema)(integer) /** * Creates Plutus bounded bytes from hex string @@ -272,7 +292,7 @@ export const int = (integer: bigint): Int => integer * @since 2.0.0 * @category constructors */ -export const bytearray = (bytes: string): ByteArray => bytes +export const bytearray = (bytes: string): ByteArray => Schema.decodeSync(ByteArray)(bytes) /** * Pattern matching helper for Constr types @@ -338,19 +358,19 @@ export const matchData = ( * * @since 2.0.0 */ -export const genPlutusData = (depth: number = 3): FastCheck.Arbitrary => { +export const arbitraryPlutusData = (depth: number = 3): FastCheck.Arbitrary => { if (depth <= 0) { // Base cases: PlutusBigInt or PlutusBytes - return FastCheck.oneof(genPlutusBigInt(), genPlutusBytes()) + return FastCheck.oneof(arbitraryPlutusBigInt(), arbitraryPlutusBytes()) } // Recursive cases with decreasing depth return FastCheck.oneof( - genPlutusBigInt(), - genPlutusBytes(), - genConstr(depth - 1), - genPlutusList(depth - 1), - genPlutusMap(depth - 1) + arbitraryPlutusBigInt(), + arbitraryPlutusBytes(), + arbitraryConstr(depth - 1), + arbitraryPlutusList(depth - 1), + arbitraryPlutusMap(depth - 1) ) } @@ -361,11 +381,11 @@ export const genPlutusData = (depth: number = 3): FastCheck.Arbitrary => { * * @since 2.0.0 */ -export const genPlutusBytes = (): FastCheck.Arbitrary => +export const arbitraryPlutusBytes = (): FastCheck.Arbitrary => FastCheck.uint8Array({ minLength: 0, // Allow empty arrays (valid for PlutusBytes) maxLength: 32 // Max 32 bytes - }).map((bytes) => bytearray(Bytes.Codec.Decode.bytesLenient(bytes))) + }).map((bytes) => bytearray(Schema.decodeSync(Bytes.FromBytesLenient)(bytes))) /** * Creates an arbitrary that generates PlutusBigInt values @@ -374,7 +394,7 @@ export const genPlutusBytes = (): FastCheck.Arbitrary => * * @since 2.0.0 */ -export const genPlutusBigInt = (): FastCheck.Arbitrary => FastCheck.bigInt().map((value) => int(value)) +export const arbitraryPlutusBigInt = (): FastCheck.Arbitrary => FastCheck.bigInt().map((value) => int(value)) /** * Creates an arbitrary that generates PlutusList values @@ -383,8 +403,8 @@ export const genPlutusBigInt = (): FastCheck.Arbitrary => FastCheck.bigInt( * * @since 2.0.0 */ -export const genPlutusList = (depth: number): FastCheck.Arbitrary => - FastCheck.array(genPlutusData(depth), { +export const arbitraryPlutusList = (depth: number): FastCheck.Arbitrary => + FastCheck.array(arbitraryPlutusData(depth), { minLength: 0, maxLength: 5 }).map((value) => list(value)) @@ -396,10 +416,10 @@ export const genPlutusList = (depth: number): FastCheck.Arbitrary => * * @since 2.0.0 */ -export const genConstr = (depth: number): FastCheck.Arbitrary => +export const arbitraryConstr = (depth: number): FastCheck.Arbitrary => FastCheck.tuple( FastCheck.bigInt({ min: 0n, max: 2n ** 64n - 1n }), - FastCheck.array(genPlutusData(depth), { + FastCheck.array(arbitraryPlutusData(depth), { minLength: 0, maxLength: 5 }) @@ -416,10 +436,10 @@ export const genConstr = (depth: number): FastCheck.Arbitrary => * * @since 2.0.0 */ -export const genPlutusMap = (depth: number): FastCheck.Arbitrary => { +export const arbitraryPlutusMap = (depth: number): FastCheck.Arbitrary => { // Helper to create key-value pairs with unique keys const uniqueKeyValuePairs = (keyGen: FastCheck.Arbitrary, maxSize: number) => - FastCheck.uniqueArray(FastCheck.tuple(keyGen, genPlutusData(depth > 0 ? depth - 1 : 0)), { + FastCheck.uniqueArray(FastCheck.tuple(keyGen, arbitraryPlutusData(depth > 0 ? depth - 1 : 0)), { minLength: 0, maxLength: maxSize * 2, // Generate more than needed to increase chance of unique keys selector: (pair) => { @@ -428,50 +448,60 @@ export const genPlutusMap = (depth: number): FastCheck.Arbitrary => { const keyStr = typeof pair[0] === "bigint" ? String(pair[0]) : JSON.stringify(pair[0]) return keyStr } - }).map((pairs) => pairs.map(([key, value]) => ({ key, value }))) + }) // PlutusBigInt keys (more frequent) - const bigIntPairs = uniqueKeyValuePairs(genPlutusBigInt(), 3) + const bigIntPairs = uniqueKeyValuePairs(arbitraryPlutusBigInt(), 3) // PlutusBytes keys (medium frequency) - const bytesPairs = uniqueKeyValuePairs(genPlutusBytes(), 3) + const bytesPairs = uniqueKeyValuePairs(arbitraryPlutusBytes(), 3) // Complex keys (less frequent) - const complexPairs = uniqueKeyValuePairs(genPlutusData(depth > 1 ? depth - 2 : 0), 2) + const complexPairs = uniqueKeyValuePairs(arbitraryPlutusData(depth > 1 ? depth - 2 : 0), 2) return FastCheck.oneof(bigIntPairs, bytesPairs, complexPairs).map((pairs) => map(pairs)) } /** - * FastCheck generators for PlutusData types + * FastCheck arbitrary for PlutusData types * * @since 2.0.0 * @category generators */ -export const generator = genPlutusData(3) +export const arbitrary = arbitraryPlutusData(3) + +// ============================================================================ +// Transformations +// ============================================================================ /** - * CBOR value representation for PlutusData - * This represents the intermediate CBOR structure that corresponds to PlutusData + * Default CBOR options for Data encoding/decoding * * @since 2.0.0 - * @category model + * @category constants */ +export const DEFAULT_CBOR_OPTIONS = CBOR.CML_DATA_DEFAULT_OPTIONS -export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - Schema.transformOrFail(Schema.Uint8ArrayFromSelf, DataSchema, { - strict: true, - encode: (toI) => - pipe(plutusDataToCBORValue(toI), (cborValue) => ParseResult.encode(CBOR.FromBytes(options))(cborValue)), - decode: (fromI) => pipe(ParseResult.decode(CBOR.FromBytes(options))(fromI), Effect.map(cborValueToPlutusData)) - }).annotations({ - identifier: "Data.FromCBORBytes" - }) +/** + * Convert a big-endian byte array to a positive bigint + * Used for CBOR tag 2/3 decoding + */ +const bytesToBigint = (bytes: Uint8Array): bigint => { + if (bytes.length === 0) { + return 0n + } -export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - Schema.compose(Bytes.FromHex, FromCBORBytes(options)).annotations({ - identifier: "Data.FromCBORHex" - }) + let result = 0n + for (let i = 0; i < bytes.length; i++) { + result = (result << 8n) | BigInt(bytes[i]) + } + + return result +} + +// ============================================================================ +// Combinators +// ============================================================================ /** * Convert PlutusData to CBORValue @@ -498,7 +528,7 @@ export const plutusDataToCBORValue = (data: Data): CBOR.CBOR => { return value }, Bytes: (bytes): CBOR.CBOR => { - return Bytes.Codec.Encode.bytesLenient(bytes) + return Schema.encodeSync(Bytes.FromBytesLenient)(bytes) }, Constr: (constr): CBOR.CBOR => { // PlutusData Constr -> CBOR tags based on index @@ -546,7 +576,7 @@ export const cborValueToPlutusData = (cborValue: CBOR.CBOR): Data => { if (cborValue.length === 0) { return "" } - return Bytes.Codec.Decode.bytes(cborValue) + return Schema.decodeSync(Bytes.FromBytes)(cborValue) } // Handle tagged values @@ -562,7 +592,7 @@ export const cborValueToPlutusData = (cborValue: CBOR.CBOR): Data => { }) } const fields = value.map(cborValueToPlutusData) - return new Constr({ index: BigInt(tag - 121), fields }) + return new Constr({ index: Numeric.Uint64Make(BigInt(tag - 121)), fields }) } // Handle alternative constructor tags (1280-1400 for indices 7-127) @@ -573,7 +603,7 @@ export const cborValueToPlutusData = (cborValue: CBOR.CBOR): Data => { }) } const fields = value.map(cborValueToPlutusData) - return new Constr({ index: BigInt(tag - 1280 + 7), fields }) + return new Constr({ index: Numeric.Uint64Make(BigInt(tag - 1280 + 7)), fields }) } // Handle general constructor tag (102) @@ -602,7 +632,7 @@ export const cborValueToPlutusData = (cborValue: CBOR.CBOR): Data => { } const fields = fieldsValue.map(cborValueToPlutusData) - return new Constr({ index: indexValue, fields }) + return new Constr({ index: Numeric.Uint64Make(indexValue), fields }) } } @@ -655,75 +685,382 @@ export const cborValueToPlutusData = (cborValue: CBOR.CBOR): Data => { } /** - * Convert a big-endian byte array to a positive bigint - * Used for CBOR tag 2/3 decoding + * CBOR value representation for PlutusData + * This represents the intermediate CBOR structure that corresponds to PlutusData + * + * @since 2.0.0 + * @category model */ -const bytesToBigint = (bytes: Uint8Array): bigint => { - if (bytes.length === 0) { - return 0n + +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS) => + Schema.transformOrFail(Schema.Uint8ArrayFromSelf, DataSchema, { + strict: true, + encode: (toI) => + pipe(plutusDataToCBORValue(toI), (cborValue) => ParseResult.encode(CBOR.FromBytes(options))(cborValue)), + decode: (fromI) => pipe(ParseResult.decode(CBOR.FromBytes(options))(fromI), ParseResult.map(cborValueToPlutusData)) + }).annotations({ + identifier: "Data.FromCBORBytes" + }) + +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS) => + Schema.compose(Bytes.FromHex, FromCBORBytes(options)).annotations({ + identifier: "Data.FromCBORHex" + }) + +// ============================================================================ +// Either Namespace +// ============================================================================ + +/** + * Either-based variants for functions that can fail. + * + * @since 2.0.0 + * @category either + */ +export namespace Either { + /** + * Encode PlutusData to CBOR bytes with Either error handling + * + * @since 2.0.0 + * @category transformation + */ + export const toCBORBytes = (data: Data, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS) => + E.mapLeft( + Schema.encodeEither(FromCBORBytes(options))(data), + (cause) => + new DataError({ + message: "Failed to encode to CBOR bytes", + cause + }) + ) + + /** + * Encode PlutusData to CBOR hex string with Either error handling + * + * @since 2.0.0 + * @category transformation + */ + export const toCBORHex = (data: Data, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS) => + E.mapLeft( + Schema.encodeEither(FromCBORHex(options))(data), + (cause) => + new DataError({ + message: "Failed to encode to CBOR hex", + cause + }) + ) + + /** + * Decode PlutusData from CBOR bytes with Either error handling + * + * @since 2.0.0 + * @category transformation + */ + export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS) => + E.mapLeft( + Schema.decodeEither(FromCBORBytes(options))(bytes), + (cause) => + new DataError({ + message: "Failed to decode CBOR bytes", + cause + }) + ) + + /** + * Decode PlutusData from CBOR hex string with Effect error handling + * + * @since 2.0.0 + * @category transformation + */ + export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS) => + E.mapLeft( + Schema.decodeEither(FromCBORHex(options))(hex), + (cause) => new DataError({ message: "Failed to decode CBOR hex", cause }) + ) + + /** + * Transform data to Data using a schema with Either error handling + * + * @since 2.0.0 + * @category transformation + */ + export const toData = + (schema: Schema.Schema) => + (data: A) => + E.mapLeft( + Schema.encodeEither(schema)(data), + (cause) => + new DataError({ + message: "Failed to encode to Data", + cause + }) + ) + + /** + * Transform Data back from a schema with Either error handling + * + * @since 2.0.0 + * @category transformation + */ + export const fromData = + (schema: Schema.Schema) => + (data: Data) => + E.mapLeft( + Schema.decodeEither(schema)(data), + (cause) => + new DataError({ + message: "Failed to decode from Data", + cause + }) + ) + + /** + * Create a schema that transforms from a custom type to Data and provides CBOR encoding + * + * @since 2.0.0 + * @category combinators + */ + export const withSchema = (schema: Schema.Schema, options: CBOR.CodecOptions = DEFAULT_CBOR_OPTIONS) => { + const _FromCBORHex = Schema.compose(FromCBORHex(options), schema) + const _FromCBORBytes = Schema.compose(FromCBORBytes(options), schema) + + return { + /** + * Transform A to Data with Either error handling + */ + toData: (A: A) => E.mapLeft(Schema.encodeEither(schema)(A), (error) => new DataError({ cause: error })), + + /** + * Transform Data to A with Either error handling + */ + fromData: (data: Data) => + E.mapLeft(Schema.decodeEither(schema)(data), (error) => new DataError({ cause: error })), + + /** + * Transform A to CBOR hex string with Either error handling + */ + toCBORHex: (A: A) => E.mapLeft(Schema.encodeEither(_FromCBORHex)(A), (error) => new DataError({ cause: error })), + + /** + * Transform A to CBOR bytes with Either error handling + */ + toCBORBytes: (A: A) => + E.mapLeft(Schema.encodeEither(_FromCBORBytes)(A), (error) => new DataError({ cause: error })), + + /** + * Transform CBOR hex string to A with Either error handling + */ + fromCBORHex: (data: string) => + E.mapLeft(Schema.decodeEither(_FromCBORHex)(data), (error) => new DataError({ cause: error })), + + /** + * Transform CBOR bytes to A with Either error handling + */ + fromCBORBytes: (data: Uint8Array) => + E.mapLeft(Schema.decodeEither(_FromCBORBytes)(data), (error) => new DataError({ cause: error })) + } } +} - let result = 0n - for (let i = 0; i < bytes.length; i++) { - result = (result << 8n) | BigInt(bytes[i]) +/** + * Encode PlutusData to CBOR bytes + * + * @since 2.0.0 + * @category transformation + */ +export const toCBORBytes = (data: Data, options?: CBOR.CodecOptions): Uint8Array => { + try { + return Schema.encodeSync(FromCBORBytes(options))(data) + } catch (cause) { + throw new DataError({ + message: "Failed to encode to CBOR bytes", + cause + }) } +} - return result +/** + * Encode PlutusData to CBOR hex string + * + * @since 2.0.0 + * @category transformation + */ +export const toCBORHex = (data: Data, options?: CBOR.CodecOptions): string => { + try { + return Schema.encodeSync(FromCBORHex(options))(data) + } catch (cause) { + throw new DataError({ + message: "Failed to encode to CBOR hex", + cause + }) + } +} + +/** + * Decode PlutusData from CBOR bytes + * + * @since 2.0.0 + * @category transformation + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Data => { + try { + return Schema.decodeSync(FromCBORBytes(options))(bytes) + } catch (cause) { + throw new DataError({ + message: "Failed to decode CBOR bytes", + cause + }) + } } -// Function overloads for better type inference -export function Codec(params: { - schema: Schema.Schema - options?: CBOR.CodecOptions -}): ReturnType< - typeof _Codec.createEncoders< - { - toData: Schema.Schema - cborHex: Schema.Schema - cborBytes: Schema.Schema +/** + * Decode PlutusData from CBOR hex string + * + * @since 2.0.0 + * @category transformation + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Data => { + try { + return Schema.decodeSync(FromCBORHex(options))(hex) + } catch (cause) { + throw new DataError({ + message: "Failed to decode CBOR hex", + cause + }) + } +} + +/** + * Transform data to Data using a schema + * + * @since 2.0.0 + * @category transformation + */ +export const toData = + (schema: Schema.Schema) => + (data: A): Data => { + try { + return Schema.encodeSync(schema)(data) + } catch (cause) { + throw new DataError({ + message: "Failed to encode to Data", + cause + }) + } + } + +/** + * Transform Data back from a schema + * + * @since 2.0.0 + * @category transformation + */ +export const fromData = + (schema: Schema.Schema) => + (data: Data): A => { + try { + return Schema.decodeSync(schema)(data) + } catch (cause) { + throw new DataError({ + message: "Failed to decode from Data", + cause + }) + } + } + +/** + * Create a schema that transforms from a custom type to Data and provides CBOR encoding + * + * @since 2.0.0 + * @category combinators + */ +export const withSchema = (schema: Schema.Schema, options?: CBOR.CodecOptions) => { + const _FromCBORHex = Schema.compose(FromCBORHex(options), schema) + const _FromCBORBytes = Schema.compose(FromCBORBytes(options), schema) + + return { + /** + * Transform A to Data + */ + toData: (A: A): Data => { + try { + return Schema.encodeSync(schema)(A) + } catch (cause) { + throw new DataError({ + message: "Failed to encode to Data", + cause + }) + } }, - typeof DataError - > -> - -export function Codec(params?: { options?: CBOR.CodecOptions }): ReturnType< - typeof _Codec.createEncoders< - { - cborHex: Schema.Schema - cborBytes: Schema.Schema + + /** + * Transform Data to A + */ + fromData: (data: Data): A => { + try { + return Schema.decodeSync(schema)(data) + } catch (cause) { + throw new DataError({ + message: "Failed to decode from Data", + cause + }) + } }, - typeof DataError - > -> - -export function Codec(params?: { schema?: Schema.Schema; options?: CBOR.CodecOptions }) { - const schema = params?.schema - const codecOptions = params?.options || CBOR.DEFAULT_OPTIONS - - const FromHex = FromCBORHex(codecOptions) - const FromBytes = FromCBORBytes(codecOptions) - - if (schema) { - // With schema: type A -> Data B -> CBOR - const schemaToHex = Schema.compose(FromHex, schema) - const schemaToBytes = Schema.compose(FromBytes, schema) - - return _Codec.createEncoders( - { - toData: schema, - cborHex: schemaToHex, - cborBytes: schemaToBytes - }, - DataError - ) - } - // Without schema: Data -> CBOR directly - return _Codec.createEncoders( - { - cborHex: FromHex, - cborBytes: FromBytes + /** + * Transform A to CBOR hex string + */ + toCBORHex: (A: A): string => { + try { + return Schema.encodeSync(_FromCBORHex)(A) + } catch (cause) { + throw new DataError({ + message: "Failed to encode to CBOR hex", + cause + }) + } }, - DataError - ) + + /** + * Transform A to CBOR bytes + */ + toCBORBytes: (A: A): Uint8Array => { + try { + return Schema.encodeSync(_FromCBORBytes)(A) + } catch (cause) { + throw new DataError({ + message: "Failed to encode to CBOR bytes", + cause + }) + } + }, + + /** + * Transform CBOR hex string to A + */ + fromCBORHex: (data: string): A => { + try { + return Schema.decodeSync(_FromCBORHex)(data) + } catch (cause) { + throw new DataError({ + message: "Failed to decode from CBOR hex", + cause + }) + } + }, + + /** + * Transform CBOR bytes to A + */ + fromCBORBytes: (data: Uint8Array): A => { + try { + return Schema.decodeSync(_FromCBORBytes)(data) + } catch (cause) { + throw new DataError({ + message: "Failed to decode from CBOR bytes", + cause + }) + } + } + } } diff --git a/packages/evolution/src/DatumOption.ts b/packages/evolution/src/DatumOption.ts index 4e45e13e..5fb91b03 100644 --- a/packages/evolution/src/DatumOption.ts +++ b/packages/evolution/src/DatumOption.ts @@ -1,9 +1,8 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as Bytes32 from "./Bytes32.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as PlutusData from "./Data.js" /** @@ -116,19 +115,36 @@ export const getData = (datumOption: DatumOption): PlutusData.Data | undefined = isInlineDatum(datumOption) ? datumOption.data : undefined /** - * FastCheck generator for DatumOption instances. + * Check if two DatumOption instances are equal. * * @since 2.0.0 - * @category generators + * @category equality */ -export const generator = FastCheck.oneof( +export const equals = (a: DatumOption, b: DatumOption): boolean => { + if (a._tag !== b._tag) return false + if (a._tag === "DatumHash" && b._tag === "DatumHash") { + return a.hash === b.hash + } + if (a._tag === "InlineDatum" && b._tag === "InlineDatum") { + return a.data === b.data + } + return false +} + +/** + * FastCheck arbitrary for generating random DatumOption instances + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary = FastCheck.oneof( FastCheck.record({ _tag: FastCheck.constant("DatumHash" as const), hash: FastCheck.hexaString({ minLength: 64, maxLength: 64 }) }).map((props) => new DatumHash(props)), FastCheck.record({ _tag: FastCheck.constant("InlineDatum" as const), - data: PlutusData.genPlutusData() + data: PlutusData.arbitrary }).map((props) => new InlineDatum(props)) ) @@ -152,7 +168,7 @@ export const DatumOptionCDDLSchema = Schema.transformOrFail( { strict: true, encode: (toA) => - Effect.gen(function* () { + Eff.gen(function* () { const result = toA._tag === "DatumHash" ? ([0n, yield* ParseResult.encode(Bytes.FromBytes)(toA.hash)] as const) // Encode as [0, Bytes32] @@ -160,7 +176,7 @@ export const DatumOptionCDDLSchema = Schema.transformOrFail( return result }), decode: ([tag, value], _, ast) => - Effect.gen(function* () { + Eff.gen(function* () { if (tag === 0n) { // Decode as DatumHash const hash = yield* ParseResult.decode(Bytes.FromBytes)(value) @@ -176,7 +192,10 @@ export const DatumOptionCDDLSchema = Schema.transformOrFail( ) }) } -) +).annotations({ + identifier: "DatumOption.DatumOptionCDDLSchema", + description: "Transforms CBOR structure to DatumOption" +}) /** * CBOR bytes transformation schema for DatumOption. @@ -185,11 +204,14 @@ export const DatumOptionCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR DatumOptionCDDLSchema // CBOR → DatumOption - ) + ).annotations({ + identifier: "DatumOption.FromCBORBytes", + description: "Transforms CBOR bytes to DatumOption" + }) /** * CBOR hex transformation schema for DatumOption. @@ -198,17 +220,103 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → DatumOption - ) - -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - DatumOptionError - ) + FromCBORBytes(options) // Uint8Array → DatumOption + ).annotations({ + identifier: "DatumOption.FromCBORHex", + description: "Transforms CBOR hex string to DatumOption" + }) + +/** + * Effect namespace for DatumOption operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert CBOR bytes to DatumOption using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (cause) => new DatumOptionError({ message: "Failed to decode from CBOR bytes", cause }) + ) + + /** + * Convert CBOR hex string to DatumOption using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (cause) => new DatumOptionError({ message: "Failed to decode from CBOR hex", cause }) + ) + + /** + * Convert DatumOption to CBOR bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORBytes = (datumOption: DatumOption, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(datumOption), + (cause) => new DatumOptionError({ message: "Failed to encode to CBOR bytes", cause }) + ) + + /** + * Convert DatumOption to CBOR hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORHex = (datumOption: DatumOption, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(datumOption), + (cause) => new DatumOptionError({ message: "Failed to encode to CBOR hex", cause }) + ) +} + +/** + * Convert CBOR bytes to DatumOption (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): DatumOption => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Convert CBOR hex string to DatumOption (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): DatumOption => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Convert DatumOption to CBOR bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORBytes = (datumOption: DatumOption, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(datumOption, options)) + +/** + * Convert DatumOption to CBOR hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORHex = (datumOption: DatumOption, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(datumOption, options)) diff --git a/packages/evolution/src/DnsName.ts b/packages/evolution/src/DnsName.ts index 6ecf794c..6ea09c5e 100644 --- a/packages/evolution/src/DnsName.ts +++ b/packages/evolution/src/DnsName.ts @@ -1,6 +1,5 @@ -import { Data, Schema } from "effect" +import { Data, Effect as Eff, Schema } from "effect" -import * as _Codec from "./Codec.js" import * as Text128 from "./Text128.js" /** @@ -56,23 +55,137 @@ export const make = DnsName.make export const equals = (a: DnsName, b: DnsName): boolean => a === b /** - * Generate a random DnsName. + * Check if the given value is a valid DnsName * * @since 2.0.0 - * @category generators + * @category predicates */ -export const generator = Text128.generator.map((text) => make(text)) +export const isDnsName = Schema.is(DnsName) /** - * Codec utilities for DnsName encoding and decoding operations. + * FastCheck arbitrary for generating random DnsName instances. * * @since 2.0.0 - * @category encoding/decoding + * @category arbitrary */ -export const Codec = _Codec.createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - DnsNameError -) +export const arbitrary = Text128.arbitrary.map((text) => make(text)) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse DnsName from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): DnsName => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse DnsName from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): DnsName => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode DnsName to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (dnsName: DnsName): Uint8Array => + Eff.runSync(Effect.toBytes(dnsName)) + +/** + * Encode DnsName to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (dnsName: DnsName): string => + Eff.runSync(Effect.toHex(dnsName)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse DnsName from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new DnsNameError({ + message: "Failed to parse DnsName from bytes", + cause + }) + ) + ) + + /** + * Parse DnsName from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new DnsNameError({ + message: "Failed to parse DnsName from hex", + cause + }) + ) + ) + + /** + * Encode DnsName to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (dnsName: DnsName): Eff.Effect => + Schema.encode(FromBytes)(dnsName).pipe( + Eff.mapError( + (cause) => + new DnsNameError({ + message: "Failed to encode DnsName to bytes", + cause + }) + ) + ) + + /** + * Encode DnsName to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (dnsName: DnsName): Eff.Effect => + Schema.encode(FromHex)(dnsName).pipe( + Eff.mapError( + (cause) => + new DnsNameError({ + message: "Failed to encode DnsName to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/Ed25519Signature.ts b/packages/evolution/src/Ed25519Signature.ts index 5961ae8f..cee9f823 100644 --- a/packages/evolution/src/Ed25519Signature.ts +++ b/packages/evolution/src/Ed25519Signature.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes64 from "./Bytes64.js" -import { createEncoders } from "./Codec.js" /** * Error class for Ed25519Signature related operations. @@ -22,7 +21,7 @@ export class Ed25519SignatureError extends Data.TaggedError("Ed25519SignatureErr * @since 2.0.0 * @category schemas */ -export const Ed25519Signature = pipe(Bytes64.HexSchema, Schema.brand("Ed25519Signature")).annotations({ +export const Ed25519Signature = Bytes64.HexSchema.pipe(Schema.brand("Ed25519Signature")).annotations({ identifier: "Ed25519Signature" }) @@ -42,6 +41,14 @@ export const FromHex = Schema.compose( identifier: "Ed25519Signature.Hex" }) +/** + * Smart constructor for Ed25519Signature that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = Ed25519Signature.make + /** * Check if two Ed25519Signature instances are equal. * @@ -51,26 +58,140 @@ export const FromHex = Schema.compose( export const equals = (a: Ed25519Signature, b: Ed25519Signature): boolean => a === b /** - * Generate a random Ed25519Signature. + * Check if the given value is a valid Ed25519Signature + * + * @since 2.0.0 + * @category predicates + */ +export const isEd25519Signature = Schema.is(Ed25519Signature) + +/** + * FastCheck arbitrary for generating random Ed25519Signature instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes64.BYTES_LENGTH, - maxLength: Bytes64.BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes64.HEX_LENGTH, + maxLength: Bytes64.HEX_LENGTH +}).map((hex) => hex as Ed25519Signature) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for Ed25519Signature encoding and decoding operations. + * Parse Ed25519Signature from bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - Ed25519SignatureError -) +export const fromBytes = (bytes: Uint8Array): Ed25519Signature => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse Ed25519Signature from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): Ed25519Signature => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode Ed25519Signature to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (signature: Ed25519Signature): Uint8Array => + Eff.runSync(Effect.toBytes(signature)) + +/** + * Encode Ed25519Signature to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (signature: Ed25519Signature): string => + Eff.runSync(Effect.toHex(signature)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Ed25519Signature from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new Ed25519SignatureError({ + message: "Failed to parse Ed25519Signature from bytes", + cause + }) + ) + ) + + /** + * Parse Ed25519Signature from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new Ed25519SignatureError({ + message: "Failed to parse Ed25519Signature from hex", + cause + }) + ) + ) + + /** + * Encode Ed25519Signature to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (signature: Ed25519Signature): Eff.Effect => + Schema.encode(FromBytes)(signature).pipe( + Eff.mapError( + (cause) => + new Ed25519SignatureError({ + message: "Failed to encode Ed25519Signature to bytes", + cause + }) + ) + ) + + /** + * Encode Ed25519Signature to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (signature: Ed25519Signature): Eff.Effect => + Schema.encode(FromHex)(signature).pipe( + Eff.mapError( + (cause) => + new Ed25519SignatureError({ + message: "Failed to encode Ed25519Signature to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/EnterpriseAddress.ts b/packages/evolution/src/EnterpriseAddress.ts index b7ffbeb5..dd77edc7 100644 --- a/packages/evolution/src/EnterpriseAddress.ts +++ b/packages/evolution/src/EnterpriseAddress.ts @@ -1,8 +1,7 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as Bytes29 from "./Bytes29.js" -import * as _Codec from "./Codec.js" import * as Credential from "./Credential.js" import * as KeyHash from "./KeyHash.js" import * as NetworkId from "./NetworkId.js" @@ -22,20 +21,12 @@ export class EnterpriseAddressError extends Data.TaggedError("EnterpriseAddressE export class EnterpriseAddress extends Schema.TaggedClass("EnterpriseAddress")("EnterpriseAddress", { networkId: NetworkId.NetworkId, paymentCredential: Credential.Credential -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: "EnterpriseAddress", - networkId: this.networkId, - paymentCredential: this.paymentCredential - } - } -} +}) {} export const FromBytes = Schema.transformOrFail(Bytes29.BytesSchema, EnterpriseAddress, { strict: true, encode: (_, __, ___, toA) => - Effect.gen(function* () { + Eff.gen(function* () { const paymentBit = toA.paymentCredential._tag === "KeyHash" ? 0 : 1 const header = (0b01 << 6) | (0b1 << 5) | (paymentBit << 4) | (toA.networkId & 0b00001111) @@ -48,7 +39,7 @@ export const FromBytes = Schema.transformOrFail(Bytes29.BytesSchema, EnterpriseA return yield* ParseResult.succeed(result) }), decode: (_, __, ___, fromA) => - Effect.gen(function* () { + Eff.gen(function* () { const header = fromA[0] // Extract network ID from the lower 4 bits const networkId = header & 0b00001111 @@ -64,7 +55,7 @@ export const FromBytes = Schema.transformOrFail(Bytes29.BytesSchema, EnterpriseA } : { _tag: "ScriptHash", - hash: yield* ParseResult.decode(ScriptHash.BytesSchema)(fromA.slice(1, 29)) + hash: yield* ParseResult.decode(ScriptHash.FromBytes)(fromA.slice(1, 29)) } return yield* ParseResult.decode(EnterpriseAddress)({ _tag: "EnterpriseAddress", @@ -72,12 +63,29 @@ export const FromBytes = Schema.transformOrFail(Bytes29.BytesSchema, EnterpriseA paymentCredential }) }) +}).annotations({ + identifier: "EnterpriseAddress.FromBytes", + description: "Transforms raw bytes to EnterpriseAddress" }) export const FromHex = Schema.compose( Bytes.FromHex, // string → Uint8Array FromBytes // Uint8Array → EnterpriseAddress -) +).annotations({ + identifier: "EnterpriseAddress.FromHex", + description: "Transforms raw hex string to EnterpriseAddress" +}) + +/** + * Smart constructor for creating EnterpriseAddress instances + * + * @since 2.0.0 + * @category constructors + */ +export const make = (props: { + networkId: NetworkId.NetworkId + paymentCredential: Credential.Credential +}): EnterpriseAddress => new EnterpriseAddress(props) /** * Check if two EnterpriseAddress instances are equal. @@ -86,31 +94,103 @@ export const FromHex = Schema.compose( * @category equality */ export const equals = (a: EnterpriseAddress, b: EnterpriseAddress): boolean => { - return ( - a.networkId === b.networkId && - a.paymentCredential._tag === b.paymentCredential._tag && - a.paymentCredential.hash === b.paymentCredential.hash - ) + return a.networkId === b.networkId && Credential.equals(a.paymentCredential, b.paymentCredential) } /** - * Generate a random EnterpriseAddress. + * FastCheck arbitrary for generating random EnterpriseAddress instances * * @since 2.0.0 - * @category generators + * @category testing */ -export const generator = FastCheck.tuple(NetworkId.generator, Credential.generator).map( - ([networkId, paymentCredential]) => - new EnterpriseAddress({ - networkId, - paymentCredential - }) +export const arbitrary = FastCheck.tuple(NetworkId.arbitrary, Credential.arbitrary).map( + ([networkId, paymentCredential]) => make({ networkId, paymentCredential }) ) -export const Codec = _Codec.createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - EnterpriseAddressError -) +/** + * Effect namespace for EnterpriseAddress operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert bytes to EnterpriseAddress using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromBytes = (bytes: Uint8Array) => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new EnterpriseAddressError({ message: "Failed to decode from bytes", cause }) + ) + + /** + * Convert hex string to EnterpriseAddress using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromHex = (hex: string) => + Eff.mapError( + Schema.decode(FromHex)(hex), + (cause) => new EnterpriseAddressError({ message: "Failed to decode from hex", cause }) + ) + + /** + * Convert EnterpriseAddress to bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toBytes = (address: EnterpriseAddress) => + Eff.mapError( + Schema.encode(FromBytes)(address), + (cause) => new EnterpriseAddressError({ message: "Failed to encode to bytes", cause }) + ) + + /** + * Convert EnterpriseAddress to hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toHex = (address: EnterpriseAddress) => + Eff.mapError( + Schema.encode(FromHex)(address), + (cause) => new EnterpriseAddressError({ message: "Failed to encode to hex", cause }) + ) +} + +/** + * Convert bytes to EnterpriseAddress (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromBytes = (bytes: Uint8Array): EnterpriseAddress => Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert hex string to EnterpriseAddress (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromHex = (hex: string): EnterpriseAddress => Eff.runSync(Effect.fromHex(hex)) + +/** + * Convert EnterpriseAddress to bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toBytes = (address: EnterpriseAddress): Uint8Array => Eff.runSync(Effect.toBytes(address)) + +/** + * Convert EnterpriseAddress to hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toHex = (address: EnterpriseAddress): string => Eff.runSync(Effect.toHex(address)) diff --git a/packages/evolution/src/GovernanceAction.ts b/packages/evolution/src/GovernanceAction.ts new file mode 100644 index 00000000..537b5530 --- /dev/null +++ b/packages/evolution/src/GovernanceAction.ts @@ -0,0 +1,829 @@ +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" + +import * as CBOR from "./CBOR.js" +import * as Coin from "./Coin.js" +import * as RewardAccount from "./RewardAccount.js" +import * as ScriptHash from "./ScriptHash.js" +import * as TransactionHash from "./TransactionHash.js" +import * as TransactionIndex from "./TransactionIndex.js" + +/** + * Error class for GovernanceAction related operations. + * + * @since 2.0.0 + * @category errors + */ +export class GovernanceActionError extends Data.TaggedError("GovernanceActionError")<{ + message?: string + cause?: unknown +}> {} + +/** + * GovActionId schema representing a governance action identifier. + * According to Conway CDDL: gov_action_id = [transaction_id : transaction_id, gov_action_index : uint .size 2] + * + * @since 2.0.0 + * @category schemas + */ +export class GovActionId extends Schema.TaggedClass()("GovActionId", { + transactionId: TransactionHash.TransactionHash, // transaction_id (hash32) + govActionIndex: TransactionIndex.TransactionIndex // uint .size 2 (governance action index) +}) {} + +/** + * CDDL schema for GovActionId tuple structure. + * For CBOR encoding: [transaction_id: bytes, gov_action_index: uint] + * + * @since 2.0.0 + * @category schemas + */ +export const GovActionIdCDDL = Schema.Tuple( + CBOR.ByteArray, // transaction_id as bytes + CBOR.Integer // gov_action_index as uint +) + +/** + * CDDL transformation schema for GovActionId. + * + * @since 2.0.0 + * @category schemas + */ +export const GovActionIdFromCDDL = Schema.transformOrFail(GovActionIdCDDL, GovActionId, { + strict: true, + encode: (_, __, ___, toA) => + Eff.gen(function* () { + // Convert domain types to CBOR types + const transactionIdBytes = yield* ParseResult.encode(TransactionHash.FromBytes)(toA.transactionId) + const indexNumber = yield* ParseResult.encode(TransactionIndex.TransactionIndex)(toA.govActionIndex) + return [transactionIdBytes, BigInt(indexNumber)] as const + }), + decode: (fromA) => + Eff.gen(function* () { + const [transactionIdBytes, govActionIndexNumber] = fromA + // Convert CBOR types to domain types + const transactionId = yield* ParseResult.decode(TransactionHash.FromBytes)(transactionIdBytes) + const govActionIndex = yield* ParseResult.decode(TransactionIndex.TransactionIndex)(Number(govActionIndexNumber)) + return new GovActionId({ transactionId, govActionIndex }) + }) +}) + +/** + * Parameter change governance action schema. + * According to Conway CDDL: parameter_change_action = + * (0, gov_action_id/ nil, protocol_param_update, policy_hash/ nil) + * + * @since 2.0.0 + * @category schemas + */ +export class ParameterChangeAction extends Schema.TaggedClass()("ParameterChangeAction", { + govActionId: Schema.NullOr(GovActionId), // gov_action_id / nil + protocolParamUpdate: CBOR.RecordSchema, // protocol_param_update as CBOR record + policyHash: Schema.NullOr(ScriptHash.ScriptHash) // policy_hash / nil +}) {} + +/** + * CDDL schema for ParameterChangeAction tuple structure. + * Maps to: (0, gov_action_id/ nil, protocol_param_update, policy_hash/ nil) + * + * @since 2.0.0 + * @category schemas + */ +export const ParameterChangeActionCDDL = Schema.Tuple( + Schema.Literal(0), // action type + Schema.NullOr(GovActionIdCDDL), // gov_action_id / nil + CBOR.RecordSchema, // protocol_param_update + Schema.NullOr(CBOR.ByteArray) // policy_hash / nil +) + +/** + * CDDL transformation schema for ParameterChangeAction. + * + * @since 2.0.0 + * @category schemas + */ +export const ParameterChangeActionFromCDDL = Schema.transformOrFail( + ParameterChangeActionCDDL, + Schema.typeSchema(ParameterChangeAction), + { + strict: true, + encode: (action) => + Eff.gen(function* () { + const govActionId = action.govActionId + ? yield* ParseResult.encode(GovActionIdFromCDDL)(action.govActionId) + : null + const protocolParamUpdate = yield* ParseResult.encode(CBOR.RecordSchema)(action.protocolParamUpdate) + const policyHash = action.policyHash ? yield* ParseResult.encode(ScriptHash.FromBytes)(action.policyHash) : null + + // Return as CBOR tuple + return [0, govActionId, protocolParamUpdate, policyHash] as const + }), + decode: (cddl) => + Eff.gen(function* () { + const [, govActionIdCDDL, protocolParamUpdate, policyHash] = cddl + const govActionId = govActionIdCDDL ? yield* ParseResult.decode(GovActionIdFromCDDL)(govActionIdCDDL) : null + const policyHashValue = policyHash ? yield* ParseResult.decode(ScriptHash.FromBytes)(policyHash) : null + + return new ParameterChangeAction({ + govActionId, + protocolParamUpdate, + policyHash: policyHashValue + }) + }) + } +) + +/** + * Hard fork initiation governance action schema. + * According to Conway CDDL: hard_fork_initiation_action = + * (1, gov_action_id/ nil, protocol_version, policy_hash/ nil) + * + * @since 2.0.0 + * @category schemas + */ +export class HardForkInitiationAction extends Schema.TaggedClass()( + "HardForkInitiationAction", + { + govActionId: Schema.NullOr(GovActionId), // gov_action_id / nil + protocolVersion: Schema.Tuple(Schema.Number, Schema.Number), // protocol_version = [major, minor] + policyHash: Schema.NullOr(ScriptHash.ScriptHash) // policy_hash / nil + } +) {} + +/** + * CDDL schema for HardForkInitiationAction tuple structure. + * Maps to: (1, gov_action_id/ nil, protocol_version, policy_hash/ nil) + * + * @since 2.0.0 + * @category schemas + */ +export const HardForkInitiationActionCDDL = Schema.Tuple( + Schema.Literal(1), // action type + Schema.NullOr(GovActionIdCDDL), // gov_action_id / nil + Schema.Tuple(CBOR.Integer, CBOR.Integer), // protocol_version = [major, minor] + Schema.NullOr(CBOR.ByteArray) // policy_hash / nil +) + +/** + * CDDL transformation schema for HardForkInitiationAction. + * + * @since 2.0.0 + * @category schemas + */ +export const HardForkInitiationActionFromCDDL = Schema.transformOrFail( + HardForkInitiationActionCDDL, + Schema.typeSchema(HardForkInitiationAction), + { + strict: true, + encode: (action) => + Eff.gen(function* () { + const govActionId = action.govActionId + ? yield* ParseResult.encode(GovActionIdFromCDDL)(action.govActionId) + : null + const policyHash = action.policyHash ? yield* ParseResult.encode(ScriptHash.FromBytes)(action.policyHash) : null + + // Return as CBOR tuple + return [ + 1, + govActionId, + [BigInt(action.protocolVersion[0]), BigInt(action.protocolVersion[1])], + policyHash + ] as const + }), + decode: (cddl) => + Eff.gen(function* () { + const [, govActionIdCDDL, protocolVersion, policyHash] = cddl + const govActionId = govActionIdCDDL ? yield* ParseResult.decode(GovActionIdFromCDDL)(govActionIdCDDL) : null + const policyHashValue = policyHash ? yield* ParseResult.decode(ScriptHash.FromBytes)(policyHash) : null + + return new HardForkInitiationAction({ + govActionId, + protocolVersion: [Number(protocolVersion[0]), Number(protocolVersion[1])], + policyHash: policyHashValue + }) + }) + } +) + +/** + * Treasury withdrawals governance action schema. + * According to Conway CDDL: treasury_withdrawals_action = + * (2, { * reward_account => coin }, policy_hash/ nil) + * + * @since 2.0.0 + * @category schemas + */ +export class TreasuryWithdrawalsAction extends Schema.TaggedClass()( + "TreasuryWithdrawalsAction", + { + withdrawals: Schema.MapFromSelf({ + key: RewardAccount.RewardAccount, + value: Coin.Coin + }), + policyHash: Schema.NullOr(ScriptHash.ScriptHash) // policy_hash / nil + } +) {} + +/** + * CDDL schema for TreasuryWithdrawalsAction tuple structure. + * Maps to: (2, { * reward_account => coin }, policy_hash/ nil) + * + * @since 2.0.0 + * @category schemas + */ +export const TreasuryWithdrawalsActionCDDL = Schema.Tuple( + Schema.Literal(2), // action type + Schema.MapFromSelf({ + key: CBOR.ByteArray, // reward_account as bytes + value: CBOR.Integer // coin as bigint + }), + Schema.NullOr(CBOR.ByteArray) // policy_hash / nil +) + +/** + * CDDL transformation schema for TreasuryWithdrawalsAction. + * + * @since 2.0.0 + * @category schemas + */ +export const TreasuryWithdrawalsActionFromCDDL = Schema.transformOrFail( + TreasuryWithdrawalsActionCDDL, + Schema.typeSchema(TreasuryWithdrawalsAction), + { + strict: true, + encode: (action) => + Eff.gen(function* () { + const withdrawals = new Map() + for (const [rewardAccount, coin] of action.withdrawals) { + const rewardAccountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(rewardAccount) + withdrawals.set(rewardAccountBytes, coin) + } + const policyHash = action.policyHash ? yield* ParseResult.encode(ScriptHash.FromBytes)(action.policyHash) : null + + // Return as CBOR tuple + return [2, withdrawals, policyHash] as const + }), + decode: (cddl) => + Eff.gen(function* () { + const [, withdrawals, policyHash] = cddl + const policyHashValue = policyHash ? yield* ParseResult.decode(ScriptHash.FromBytes)(policyHash) : null + const withdrawalsMap = new Map() + for (const [rewardAccountBytes, coin] of withdrawals) { + const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(rewardAccountBytes) + withdrawalsMap.set(rewardAccount, coin) + } + + return new TreasuryWithdrawalsAction({ + withdrawals: withdrawalsMap, + policyHash: policyHashValue + }) + }) + } +) + +/** + * No confidence governance action schema. + * According to Conway CDDL: no_confidence = + * (3, gov_action_id/ nil) + * + * @since 2.0.0 + * @category schemas + */ +export class NoConfidenceAction extends Schema.TaggedClass()("NoConfidenceAction", { + govActionId: Schema.NullOr(GovActionId) // gov_action_id / nil +}) {} + +/** + * CDDL schema for NoConfidenceAction tuple structure. + * Maps to: (3, gov_action_id/ nil) + * + * @since 2.0.0 + * @category schemas + */ +export const NoConfidenceActionCDDL = Schema.Tuple( + Schema.Literal(3), // action type + Schema.NullOr(GovActionIdCDDL) // gov_action_id / nil +) + +/** + * CDDL transformation schema for NoConfidenceAction. + * + * @since 2.0.0 + * @category schemas + */ +export const NoConfidenceActionFromCDDL = Schema.transformOrFail( + NoConfidenceActionCDDL, + Schema.typeSchema(NoConfidenceAction), + { + strict: true, + encode: (action) => + Eff.gen(function* () { + const govActionId = action.govActionId + ? yield* ParseResult.encode(GovActionIdFromCDDL)(action.govActionId) + : null + + // Return as CBOR tuple + return [3, govActionId] as const + }), + decode: (cddl) => + Eff.gen(function* () { + const [, govActionIdCDDL] = cddl + const govActionId = govActionIdCDDL ? yield* ParseResult.decode(GovActionIdFromCDDL)(govActionIdCDDL) : null + + return new NoConfidenceAction({ + govActionId + }) + }) + } +) + +/** + * Update committee governance action schema. + * According to Conway CDDL: update_committee = + * (4, gov_action_id/ nil, set, { * committee_cold_credential => committee_hot_credential }, unit_interval) + * + * @since 2.0.0 + * @category schemas + */ +export class UpdateCommitteeAction extends Schema.TaggedClass()("UpdateCommitteeAction", { + govActionId: Schema.NullOr(GovActionId), // gov_action_id / nil + membersToRemove: Schema.Array(CBOR.CBORSchema), // set + membersToAdd: CBOR.MapSchema, // { * committee_cold_credential => committee_hot_credential } + threshold: CBOR.CBORSchema // unit_interval +}) {} + +/** + * CDDL schema for UpdateCommitteeAction tuple structure. + * Maps to: (4, gov_action_id/ nil, set, { * committee_cold_credential => committee_hot_credential }, unit_interval) + * + * @since 2.0.0 + * @category schemas + */ +export const UpdateCommitteeActionCDDL = Schema.Tuple( + Schema.Literal(4), // action type + Schema.NullOr(GovActionIdCDDL), // gov_action_id / nil + Schema.Array(CBOR.CBORSchema), // set + CBOR.MapSchema, // { * committee_cold_credential => committee_hot_credential } + CBOR.CBORSchema // unit_interval +) + +/** + * CDDL transformation schema for UpdateCommitteeAction. + * + * @since 2.0.0 + * @category schemas + */ +export const UpdateCommitteeActionFromCDDL = Schema.transformOrFail( + UpdateCommitteeActionCDDL, + Schema.typeSchema(UpdateCommitteeAction), + { + strict: true, + encode: (action) => + Eff.gen(function* () { + const govActionId = action.govActionId + ? yield* ParseResult.encode(GovActionIdFromCDDL)(action.govActionId) + : null + const membersToRemove = yield* ParseResult.encode(Schema.Array(CBOR.CBORSchema))(action.membersToRemove) + const membersToAdd = yield* ParseResult.encode(CBOR.MapSchema)(action.membersToAdd) + const threshold = yield* ParseResult.encode(CBOR.CBORSchema)(action.threshold) + + // Return as CBOR tuple + return [4, govActionId, membersToRemove, membersToAdd, threshold] as const + }), + decode: (cddl) => + Eff.gen(function* () { + const [, govActionIdCDDL, membersToRemove, membersToAdd, threshold] = cddl + const govActionId = govActionIdCDDL ? yield* ParseResult.decode(GovActionIdFromCDDL)(govActionIdCDDL) : null + + return new UpdateCommitteeAction({ + govActionId, + membersToRemove, + membersToAdd, + threshold + }) + }) + } +) + +/** + * New constitution governance action schema. + * According to Conway CDDL: new_constitution = + * (5, gov_action_id/ nil, constitution) + * + * @since 2.0.0 + * @category schemas + */ +export class NewConstitutionAction extends Schema.TaggedClass()("NewConstitutionAction", { + govActionId: Schema.NullOr(GovActionId), // gov_action_id / nil + constitution: CBOR.CBORSchema // constitution as CBOR +}) {} + +/** + * CDDL schema for NewConstitutionAction tuple structure. + * Maps to: (5, gov_action_id/ nil, constitution) + * + * @since 2.0.0 + * @category schemas + */ +export const NewConstitutionActionCDDL = Schema.Tuple( + Schema.Literal(5), // action type + Schema.NullOr(GovActionIdCDDL), // gov_action_id / nil + CBOR.CBORSchema // constitution +) + +/** + * CDDL transformation schema for NewConstitutionAction. + * + * @since 2.0.0 + * @category schemas + */ +export const NewConstitutionActionFromCDDL = Schema.transformOrFail( + NewConstitutionActionCDDL, + Schema.typeSchema(NewConstitutionAction), + { + strict: true, + encode: (action) => + Eff.gen(function* () { + const govActionId = action.govActionId + ? yield* ParseResult.encode(GovActionIdFromCDDL)(action.govActionId) + : null + const constitution = yield* ParseResult.encode(CBOR.CBORSchema)(action.constitution) + + // Return as CBOR tuple + return [5, govActionId, constitution] as const + }), + decode: (cddl) => + Eff.gen(function* () { + const [, govActionIdCDDL, constitution] = cddl + const govActionId = govActionIdCDDL ? yield* ParseResult.decode(GovActionIdFromCDDL)(govActionIdCDDL) : null + + return new NewConstitutionAction({ + govActionId, + constitution + }) + }) + } +) + +/** + * Info governance action schema. + * According to Conway CDDL: info_action = (6) + * + * @since 2.0.0 + * @category schemas + */ +export class InfoAction extends Schema.TaggedClass()("InfoAction", { + // Info action has no additional data +}) {} + +/** + * CDDL schema for InfoAction tuple structure. + * Maps to: (6) + * + * @since 2.0.0 + * @category schemas + */ +export const InfoActionCDDL = Schema.Tuple( + Schema.Literal(6) // action type +) + +/** + * CDDL transformation schema for InfoAction. + * + * @since 2.0.0 + * @category schemas + */ +export const InfoActionFromCDDL = Schema.transformOrFail(InfoActionCDDL, Schema.typeSchema(InfoAction), { + strict: true, + encode: (_action) => + Eff.gen(function* () { + // Return as CBOR tuple + return [6] as const + }), + decode: (_cddl) => + Eff.gen(function* () { + return new InfoAction({}) + }) +}) + +/** + * GovernanceAction union schema based on Conway CDDL specification. + * + * ``` + * governance_action = + * [ 0, parameter_change_action ] + * / [ 1, hard_fork_initiation_action ] + * / [ 2, treasury_withdrawals_action ] + * / [ 3, no_confidence ] + * / [ 4, update_committee ] + * / [ 5, new_constitution ] + * / [ 6, info_action ] + * ``` + * + * @since 2.0.0 + * @category schemas + */ +export const GovernanceAction = Schema.Union( + ParameterChangeAction, + HardForkInitiationAction, + TreasuryWithdrawalsAction, + NoConfidenceAction, + UpdateCommitteeAction, + NewConstitutionAction, + InfoAction +) + +/** + * Type alias for GovernanceAction. + * + * @since 2.0.0 + * @category model + */ +export type GovernanceAction = Schema.Schema.Type + +/** + * CDDL schema for GovernanceAction tuple structure. + * Maps action types to their data according to Conway specification. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = Schema.Union( + ParameterChangeActionCDDL, + HardForkInitiationActionCDDL, + TreasuryWithdrawalsActionCDDL, + NoConfidenceActionCDDL, + UpdateCommitteeActionCDDL, + NewConstitutionActionCDDL, + InfoActionCDDL +) + +/** + * CDDL transformation schema for GovernanceAction. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.Union( + ParameterChangeActionFromCDDL, + HardForkInitiationActionFromCDDL, + TreasuryWithdrawalsActionFromCDDL, + NoConfidenceActionFromCDDL, + UpdateCommitteeActionFromCDDL, + NewConstitutionActionFromCDDL, + InfoActionFromCDDL +) + +/** + * Check if two GovernanceAction instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: GovernanceAction, b: GovernanceAction): boolean => { + if (a._tag !== b._tag) return false + + switch (a._tag) { + case "ParameterChangeAction": + return ( + b._tag === "ParameterChangeAction" && + JSON.stringify(a.protocolParamUpdate) === JSON.stringify(b.protocolParamUpdate) && + JSON.stringify(a.policyHash) === JSON.stringify(b.policyHash) && + JSON.stringify(a.govActionId) === JSON.stringify(b.govActionId) + ) + case "HardForkInitiationAction": + return ( + b._tag === "HardForkInitiationAction" && + JSON.stringify(a.protocolVersion) === JSON.stringify(b.protocolVersion) && + JSON.stringify(a.policyHash) === JSON.stringify(b.policyHash) && + JSON.stringify(a.govActionId) === JSON.stringify(b.govActionId) + ) + case "TreasuryWithdrawalsAction": + return ( + b._tag === "TreasuryWithdrawalsAction" && + JSON.stringify(a.withdrawals) === JSON.stringify(b.withdrawals) && + JSON.stringify(a.policyHash) === JSON.stringify(b.policyHash) + ) + case "NoConfidenceAction": + return b._tag === "NoConfidenceAction" && JSON.stringify(a.govActionId) === JSON.stringify(b.govActionId) + case "UpdateCommitteeAction": + return ( + b._tag === "UpdateCommitteeAction" && + JSON.stringify(a.membersToRemove) === JSON.stringify(b.membersToRemove) && + JSON.stringify(a.membersToAdd) === JSON.stringify(b.membersToAdd) && + JSON.stringify(a.threshold) === JSON.stringify(b.threshold) && + JSON.stringify(a.govActionId) === JSON.stringify(b.govActionId) + ) + case "NewConstitutionAction": + return ( + b._tag === "NewConstitutionAction" && + JSON.stringify(a.constitution) === JSON.stringify(b.constitution) && + JSON.stringify(a.govActionId) === JSON.stringify(b.govActionId) + ) + case "InfoAction": + return b._tag === "InfoAction" + } +} + +/** + * Create a parameter change governance action. + * + * @since 2.0.0 + * @category constructors + */ +export const makeParameterChange = ( + govActionId: GovActionId | null, + protocolParamUpdate: Record, + policyHash: ScriptHash.ScriptHash | null = null +): ParameterChangeAction => + new ParameterChangeAction({ + govActionId, + protocolParamUpdate, + policyHash + }) + +/** + * Create a hard fork initiation governance action. + * + * @since 2.0.0 + * @category constructors + */ +export const makeHardForkInitiation = ( + govActionId: GovActionId | null, + protocolVersion: readonly [number, number], + policyHash: ScriptHash.ScriptHash | null = null +): HardForkInitiationAction => + new HardForkInitiationAction({ + govActionId, + protocolVersion, + policyHash + }) + +/** + * Create a treasury withdrawals governance action. + * + * @since 2.0.0 + * @category constructors + */ +export const makeTreasuryWithdrawals = ( + withdrawals: Map, + policyHash: ScriptHash.ScriptHash | null = null +): TreasuryWithdrawalsAction => + new TreasuryWithdrawalsAction({ + withdrawals, + policyHash + }) + +/** + * Create a no confidence governance action. + * + * @since 2.0.0 + * @category constructors + */ +export const makeNoConfidence = (govActionId: GovActionId | null): NoConfidenceAction => + new NoConfidenceAction({ + govActionId + }) + +/** + * Create an update committee governance action. + * + * @since 2.0.0 + * @category constructors + */ +export const makeUpdateCommittee = ( + govActionId: GovActionId | null, + membersToRemove: ReadonlyArray, + membersToAdd: ReadonlyMap, + threshold: CBOR.CBOR +): UpdateCommitteeAction => + new UpdateCommitteeAction({ + govActionId, + membersToRemove, + membersToAdd, + threshold + }) + +/** + * Create a new constitution governance action. + * + * @since 2.0.0 + * @category constructors + */ +export const makeNewConstitution = ( + govActionId: GovActionId | null, + constitution: CBOR.CBOR +): NewConstitutionAction => + new NewConstitutionAction({ + govActionId, + constitution + }) + +/** + * Create an info governance action. + * + * @since 2.0.0 + * @category constructors + */ +export const makeInfo = (): InfoAction => new InfoAction({}) + +/** + * FastCheck arbitrary for GovernanceAction. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.oneof( + FastCheck.constant(makeNoConfidence(null)), + FastCheck.constant(makeInfo()) +) + +/** + * Check if a value is a valid GovernanceAction. + * + * @since 2.0.0 + * @category predicates + */ +export const is = Schema.is(GovernanceAction) + +/** + * Type guards for each governance action variant. + * + * @since 2.0.0 + * @category type guards + */ +export const isParameterChangeAction = (action: GovernanceAction): action is ParameterChangeAction => + action._tag === "ParameterChangeAction" + +export const isHardForkInitiationAction = (action: GovernanceAction): action is HardForkInitiationAction => + action._tag === "HardForkInitiationAction" + +export const isTreasuryWithdrawalsAction = (action: GovernanceAction): action is TreasuryWithdrawalsAction => + action._tag === "TreasuryWithdrawalsAction" + +export const isNoConfidenceAction = (action: GovernanceAction): action is NoConfidenceAction => + action._tag === "NoConfidenceAction" + +export const isUpdateCommitteeAction = (action: GovernanceAction): action is UpdateCommitteeAction => + action._tag === "UpdateCommitteeAction" + +export const isNewConstitutionAction = (action: GovernanceAction): action is NewConstitutionAction => + action._tag === "NewConstitutionAction" + +export const isInfoAction = (action: GovernanceAction): action is InfoAction => action._tag === "InfoAction" + +/** + * Pattern matching utility for GovernanceAction. + * + * @since 2.0.0 + * @category pattern matching + */ +export const match = ( + action: GovernanceAction, + patterns: { + ParameterChangeAction: ( + govActionId: GovActionId | null, + protocolParams: Record, + policyHash: ScriptHash.ScriptHash | null + ) => R + HardForkInitiationAction: ( + govActionId: GovActionId | null, + protocolVersion: readonly [number, number], + policyHash: ScriptHash.ScriptHash | null + ) => R + TreasuryWithdrawalsAction: ( + withdrawals: Map, + policyHash: ScriptHash.ScriptHash | null + ) => R + NoConfidenceAction: (govActionId: GovActionId | null) => R + UpdateCommitteeAction: ( + govActionId: GovActionId | null, + membersToRemove: ReadonlyArray, + membersToAdd: ReadonlyMap, + threshold: CBOR.CBOR + ) => R + NewConstitutionAction: (govActionId: GovActionId | null, constitution: CBOR.CBOR) => R + InfoAction: () => R + } +): R => { + switch (action._tag) { + case "ParameterChangeAction": + return patterns.ParameterChangeAction(action.govActionId, action.protocolParamUpdate, action.policyHash) + case "HardForkInitiationAction": + return patterns.HardForkInitiationAction(action.govActionId, action.protocolVersion, action.policyHash) + case "TreasuryWithdrawalsAction": + return patterns.TreasuryWithdrawalsAction(action.withdrawals, action.policyHash) + case "NoConfidenceAction": + return patterns.NoConfidenceAction(action.govActionId) + case "UpdateCommitteeAction": + return patterns.UpdateCommitteeAction( + action.govActionId, + action.membersToRemove, + action.membersToAdd, + action.threshold + ) + case "NewConstitutionAction": + return patterns.NewConstitutionAction(action.govActionId, action.constitution) + case "InfoAction": + return patterns.InfoAction() + } +} diff --git a/packages/evolution/src/Hash28.ts b/packages/evolution/src/Hash28.ts index 87a3cc5a..74e3ad86 100644 --- a/packages/evolution/src/Hash28.ts +++ b/packages/evolution/src/Hash28.ts @@ -1,7 +1,6 @@ -import { Data, Schema } from "effect" +import { Data, Effect as Eff, Schema } from "effect" import * as Bytes from "./Bytes.js" -import * as _Codec from "./Codec.js" export class Hash28Error extends Data.TaggedError("Hash28Error")<{ message?: string @@ -9,8 +8,8 @@ export class Hash28Error extends Data.TaggedError("Hash28Error")<{ }> {} // Add constants following the style guide -export const HASH28_BYTES_LENGTH = 28 -export const HASH28_HEX_LENGTH = 56 +export const BYTES_LENGTH = 28 +export const HEX_LENGTH = 56 /** * Schema for Hash28 bytes with 28-byte length validation. @@ -19,11 +18,14 @@ export const HASH28_HEX_LENGTH = 56 * @category schemas */ export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( - Schema.filter((a) => a.length === HASH28_BYTES_LENGTH) + Schema.filter((a) => a.length === BYTES_LENGTH) ).annotations({ identifier: "Hash28.Bytes", + title: "28-byte Hash Array", + description: "A Uint8Array containing exactly 28 bytes", message: (issue) => - `Hash28 bytes must be exactly ${HASH28_BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}` + `Hash28 bytes must be exactly ${BYTES_LENGTH} bytes, got ${(issue.actual as Uint8Array).length}`, + examples: [new Uint8Array(28).fill(0)], }) /** @@ -32,13 +34,16 @@ export const BytesSchema = Schema.Uint8ArrayFromSelf.pipe( * @since 2.0.0 * @category schemas */ -export const HexSchema = Bytes.HexSchema.pipe(Schema.filter((a: string) => a.length === HASH28_HEX_LENGTH)).annotations( - { - identifier: "Hash28.Hex", - message: (issue) => - `Hash28 hex must be exactly ${HASH28_HEX_LENGTH} characters, got ${(issue.actual as string).length}` - } -) +export const HexSchema = Bytes.HexSchema.pipe( + Schema.filter((a) => a.length === HEX_LENGTH) +).annotations({ + identifier: "Hash28.Hex", + title: "28-byte Hash Hex String", + description: "A hexadecimal string representing exactly 28 bytes (56 characters)", + message: (issue) => + `Hash28 hex must be exactly ${HEX_LENGTH} characters, got ${(issue.actual as string).length}`, + examples: ["a".repeat(56)], +}) /** * Schema for variable-length byte arrays from 0 to 28 bytes. @@ -48,10 +53,10 @@ export const HexSchema = Bytes.HexSchema.pipe(Schema.filter((a: string) => a.len * @category schemas */ export const VariableBytesSchema = Schema.Uint8ArrayFromSelf.pipe( - Schema.filter((a) => a.length >= 0 && a.length <= HASH28_BYTES_LENGTH) + Schema.filter((a) => a.length >= 0 && a.length <= BYTES_LENGTH) ).annotations({ message: (issue) => - `must be a byte array of length 0 to ${HASH28_BYTES_LENGTH}, but got ${(issue.actual as Uint8Array).length}`, + `must be a byte array of length 0 to ${BYTES_LENGTH}, but got ${(issue.actual as Uint8Array).length}`, identifier: "Hash28.VariableBytes" }) @@ -63,10 +68,10 @@ export const VariableBytesSchema = Schema.Uint8ArrayFromSelf.pipe( * @category schemas */ export const VariableHexSchema = Bytes.HexSchema.pipe( - Schema.filter((a: string) => a.length >= 0 && a.length <= HASH28_HEX_LENGTH) + Schema.filter((a) => a.length >= 0 && a.length <= HEX_LENGTH) ).annotations({ message: (issue) => - `must be a hex string of length 0 to ${HASH28_HEX_LENGTH}, but got ${(issue.actual as string).length}`, + `must be a hex string of length 0 to ${HEX_LENGTH}, but got ${(issue.actual as string).length}`, identifier: "Hash28.VariableHex" }) @@ -93,31 +98,11 @@ export const FromBytes = Schema.transform(BytesSchema, HexSchema, { } return array } -}) - -/** - * Schema transformation that converts from hex string to Uint8Array. - * Like Bytes.FromHex but with Hash28-specific length validation. - * - * @since 2.0.0 - * @category schemas - */ -export const FromHex = Schema.transform(HexSchema, BytesSchema, { - strict: true, - decode: (fromA) => { - const array = new Uint8Array(fromA.length / 2) - for (let ai = 0, hi = 0; ai < array.length; ai++, hi += 2) { - array[ai] = parseInt(fromA.slice(hi, hi + 2), 16) - } - return array - }, - encode: (toA) => { - let hex = "" - for (let i = 0; i < toA.length; i++) { - hex += toA[i].toString(16).padStart(2, "0") - } - return hex - } +}).annotations({ + identifier: "Hash28.FromBytes", + title: "Hash28 from Uint8Array", + description: "Transforms a 28-byte Uint8Array to hex string representation", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) /** @@ -144,18 +129,99 @@ export const FromVariableBytes = Schema.transform(VariableBytesSchema, VariableH } return array } +}).annotations({ + identifier: "Hash28.FromVariableBytes", + title: "Variable Hash28 from Uint8Array", + description: "Transforms variable-length byte arrays (0-28 bytes) to hex strings (0-56 chars)", + documentation: "Converts raw bytes to lowercase hexadecimal string without 0x prefix" }) /** - * Codec for Hash28 encoding and decoding operations. + * Effect namespace containing composable operations that can fail. + * All functions return Effect objects for proper error handling and composition. + */ +export namespace Effect { + /** + * Parse Hash28 from raw bytes using Effect error handling. + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new Hash28Error({ + message: "Failed to parse Hash28 from bytes", + cause + }) + ) + + /** + * Convert Hash28 hex to raw bytes using Effect error handling. + */ + export const toBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(hex), + (cause) => new Hash28Error({ + message: "Failed to encode Hash28 to bytes", + cause + }) + ) + + /** + * Parse variable-length data from raw bytes using Effect error handling. + */ + export const fromVariableBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromVariableBytes)(bytes), + (cause) => new Hash28Error({ + message: "Failed to parse variable Hash28 from bytes", + cause + }) + ) + + /** + * Convert variable-length hex to raw bytes using Effect error handling. + */ + export const toVariableBytes = (hex: string): Eff.Effect => + Eff.mapError( + Schema.encode(FromVariableBytes)(hex), + (cause) => new Hash28Error({ + message: "Failed to encode variable Hash28 to bytes", + cause + }) + ) +} + +/** + * Parse Hash28 from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): string => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert Hash28 hex to raw bytes (unsafe - throws on error). * * @since 2.0.0 - * @category encoding/decoding + * @category encoding */ -export const Codec = _Codec.createEncoders( - { - bytes: FromBytes, - variableBytes: FromVariableBytes - }, - Hash28Error -) +export const toBytes = (hex: string): Uint8Array => + Eff.runSync(Effect.toBytes(hex)) + +/** + * Parse variable-length data from raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category parsing + */ +export const fromVariableBytes = (bytes: Uint8Array): string => + Eff.runSync(Effect.fromVariableBytes(bytes)) + +/** + * Convert variable-length hex to raw bytes (unsafe - throws on error). + * + * @since 2.0.0 + * @category encoding + */ +export const toVariableBytes = (hex: string): Uint8Array => + Eff.runSync(Effect.toVariableBytes(hex)) diff --git a/packages/evolution/src/Header.ts b/packages/evolution/src/Header.ts index c2c4dc33..ac5ab2b5 100644 --- a/packages/evolution/src/Header.ts +++ b/packages/evolution/src/Header.ts @@ -94,7 +94,7 @@ export const FromCDDL = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → Header @@ -106,13 +106,13 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array FromBytes(options) // Uint8Array → Header ) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const Codec = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => createEncoders( { cborBytes: FromBytes(options), diff --git a/packages/evolution/src/HeaderBody.ts b/packages/evolution/src/HeaderBody.ts index 412ceb57..d2a37fce 100644 --- a/packages/evolution/src/HeaderBody.ts +++ b/packages/evolution/src/HeaderBody.ts @@ -1,13 +1,13 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as BlockBodyHash from "./BlockBodyHash.js" import * as BlockHeaderHash from "./BlockHeaderHash.js" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as Ed25519Signature from "./Ed25519Signature.js" import * as KESVkey from "./KESVkey.js" import * as Natural from "./Natural.js" +import * as Numeric from "./Numeric.js" import * as OperationalCert from "./OperationalCert.js" import * as ProtocolVersion from "./ProtocolVersion.js" import * as VKey from "./VKey.js" @@ -56,6 +56,36 @@ export class HeaderBody extends Schema.TaggedClass()("HeaderBody", { protocolVersion: ProtocolVersion.ProtocolVersion }) {} +/** + * Smart constructor for creating HeaderBody instances + * + * @since 2.0.0 + * @category constructors + */ +export const make = (props: { + blockNumber: number + slot: number + prevHash: BlockHeaderHash.BlockHeaderHash | null + issuerVkey: VKey.VKey + vrfVkey: VrfVkey.VrfVkey + vrfResult: VrfCert.VrfCert + blockBodySize: number + blockBodyHash: BlockBodyHash.BlockBodyHash + operationalCert: OperationalCert.OperationalCert + protocolVersion: ProtocolVersion.ProtocolVersion +}): HeaderBody => new HeaderBody({ + blockNumber: Natural.make(props.blockNumber), + slot: Natural.make(props.slot), + prevHash: props.prevHash, + issuerVkey: props.issuerVkey, + vrfVkey: props.vrfVkey, + vrfResult: props.vrfResult, + blockBodySize: Natural.make(props.blockBodySize), + blockBodyHash: props.blockBodyHash, + operationalCert: props.operationalCert, + protocolVersion: props.protocolVersion +}) + /** * Check if two HeaderBody instances are equal. * @@ -74,6 +104,42 @@ export const equals = (a: HeaderBody, b: HeaderBody): boolean => OperationalCert.equals(a.operationalCert, b.operationalCert) && ProtocolVersion.equals(a.protocolVersion, b.protocolVersion) +/** + * FastCheck arbitrary for generating random HeaderBody instances + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary = FastCheck.record({ + blockNumber: Natural.arbitrary, + slot: Natural.arbitrary, + prevHash: FastCheck.option(BlockHeaderHash.arbitrary), + issuerVkey: VKey.arbitrary, + vrfVkey: VrfVkey.arbitrary, + vrfResult: FastCheck.record({ + output: FastCheck.string(), + proof: FastCheck.string() + }), + blockBodySize: Natural.arbitrary, + blockBodyHash: BlockBodyHash.arbitrary, + operationalCert: OperationalCert.arbitrary, + protocolVersion: ProtocolVersion.arbitrary +}).map((props) => new HeaderBody({ + blockNumber: props.blockNumber, + slot: props.slot, + prevHash: props.prevHash, + issuerVkey: props.issuerVkey, + vrfVkey: props.vrfVkey, + vrfResult: new VrfCert.VrfCert({ + output: props.vrfResult.output as VrfCert.VRFOutput, + proof: props.vrfResult.proof as VrfCert.VRFProof + }), + blockBodySize: props.blockBodySize, + blockBodyHash: props.blockBodyHash, + operationalCert: props.operationalCert, + protocolVersion: props.protocolVersion +})) + /** * CDDL schema for HeaderBody. * header_body = [ @@ -115,7 +181,7 @@ export const FromCDDL = Schema.transformOrFail( { strict: true, encode: (toA) => - Effect.gen(function* () { + Eff.gen(function* () { const prevHashBytes = toA.prevHash ? yield* ParseResult.encode(BlockHeaderHash.FromBytes)(toA.prevHash) : null const issuerVkeyBytes = yield* ParseResult.encode(VKey.FromBytes)(toA.issuerVkey) const vrfVkeyBytes = yield* ParseResult.encode(VrfVkey.FromBytes)(toA.vrfVkey) @@ -155,7 +221,7 @@ export const FromCDDL = Schema.transformOrFail( [hotVkeyBytes, sequenceNumber, kesPeriod, sigmaBytes], [protocolMajor, protocolMinor] ]) => - Effect.gen(function* () { + Eff.gen(function* () { const prevHash = prevHashBytes ? yield* ParseResult.decode(BlockHeaderHash.FromBytes)(prevHashBytes) : null const issuerVkey = yield* ParseResult.decode(VKey.FromBytes)(issuerVkeyBytes) const vrfVkey = yield* ParseResult.decode(VrfVkey.FromBytes)(vrfVkeyBytes) @@ -167,8 +233,8 @@ export const FromCDDL = Schema.transformOrFail( return yield* ParseResult.decode(HeaderBody)({ _tag: "HeaderBody", - blockNumber: Natural.Natural.make(Number(blockNumber)), - slot: Natural.Natural.make(Number(slot)), + blockNumber: Natural.make(Number(blockNumber)), + slot: Natural.make(Number(slot)), prevHash, issuerVkey, vrfVkey, @@ -176,22 +242,25 @@ export const FromCDDL = Schema.transformOrFail( output: vrfOutput, proof: vrfProof }), - blockBodySize: Natural.Natural.make(Number(blockBodySize)), + blockBodySize: Natural.make(Number(blockBodySize)), blockBodyHash, operationalCert: new OperationalCert.OperationalCert({ hotVkey, - sequenceNumber, - kesPeriod, + sequenceNumber: Numeric.Uint64Make(sequenceNumber), + kesPeriod: Numeric.Uint64Make(kesPeriod), sigma }), - protocolVersion: new ProtocolVersion.ProtocolVersion({ + protocolVersion: ProtocolVersion.make({ major: Number(protocolMajor), minor: Number(protocolMinor) }) }) }) } -) +).annotations({ + identifier: "HeaderBody.FromCDDL", + description: "Transforms CBOR structure to HeaderBody" +}) /** * Check if the given value is a valid HeaderBody. @@ -207,11 +276,14 @@ export const isHeaderBody = Schema.is(HeaderBody) * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → HeaderBody - ) + ).annotations({ + identifier: "HeaderBody.FromCBORBytes", + description: "Transforms CBOR bytes to HeaderBody" + }) /** * CBOR hex transformation schema for HeaderBody. @@ -219,17 +291,103 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → HeaderBody - ) - -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - HeaderBodyError - ) + FromCBORBytes(options) // Uint8Array → HeaderBody + ).annotations({ + identifier: "HeaderBody.FromCBORHex", + description: "Transforms CBOR hex string to HeaderBody" + }) + +/** + * Effect namespace for HeaderBody operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert CBOR bytes to HeaderBody using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (cause) => new HeaderBodyError({ message: "Failed to decode from CBOR bytes", cause }) + ) + + /** + * Convert CBOR hex string to HeaderBody using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (cause) => new HeaderBodyError({ message: "Failed to decode from CBOR hex", cause }) + ) + + /** + * Convert HeaderBody to CBOR bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORBytes = (headerBody: HeaderBody, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(headerBody), + (cause) => new HeaderBodyError({ message: "Failed to encode to CBOR bytes", cause }) + ) + + /** + * Convert HeaderBody to CBOR hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORHex = (headerBody: HeaderBody, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(headerBody), + (cause) => new HeaderBodyError({ message: "Failed to encode to CBOR hex", cause }) + ) +} + +/** + * Convert CBOR bytes to HeaderBody (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): HeaderBody => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Convert CBOR hex string to HeaderBody (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): HeaderBody => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Convert HeaderBody to CBOR bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORBytes = (headerBody: HeaderBody, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(headerBody, options)) + +/** + * Convert HeaderBody to CBOR hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORHex = (headerBody: HeaderBody, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(headerBody, options)) diff --git a/packages/evolution/src/IPv4.ts b/packages/evolution/src/IPv4.ts index 72662694..f7863466 100644 --- a/packages/evolution/src/IPv4.ts +++ b/packages/evolution/src/IPv4.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Either as E, FastCheck, Schema } from "effect" import * as Bytes4 from "./Bytes4.js" -import { createEncoders } from "./Codec.js" /** * Error class for IPv4 related operations. @@ -21,7 +20,7 @@ export class IPv4Error extends Data.TaggedError("IPv4Error")<{ * @since 2.0.0 * @category schemas */ -export const IPv4 = pipe(Bytes4.HexSchema, Schema.brand("IPv4")).annotations({ +export const IPv4 = Bytes4.HexSchema.pipe(Schema.brand("IPv4")).annotations({ identifier: "IPv4" }) @@ -50,26 +49,168 @@ export const FromHex = Schema.compose( export const equals = (a: IPv4, b: IPv4): boolean => a === b /** - * Generate a random IPv4. + * Check if the given value is a valid IPv4 * * @since 2.0.0 - * @category generators + * @category predicates */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes4.BYTES_LENGTH, - maxLength: Bytes4.BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const isIPv4 = Schema.is(IPv4) /** - * Codec utilities for IPv4 encoding and decoding operations. + * FastCheck arbitrary for generating random IPv4 instances. * * @since 2.0.0 - * @category encoding/decoding + * @category arbitrary */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - IPv4Error -) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes4.HEX_LENGTH, + maxLength: Bytes4.HEX_LENGTH +}).map((hex) => hex as IPv4) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse IPv4 from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): IPv4 => { + try { + return Schema.decodeSync(FromBytes)(bytes) + } catch (cause) { + throw new IPv4Error({ + message: "Failed to parse IPv4 from bytes", + cause + }) + } +} + +/** + * Parse IPv4 from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): IPv4 => { + try { + return Schema.decodeSync(FromHex)(hex) + } catch (cause) { + throw new IPv4Error({ + message: "Failed to parse IPv4 from hex", + cause + }) + } +} + +/** + * Encode IPv4 to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (ipv4: IPv4): Uint8Array => { + try { + return Schema.encodeSync(FromBytes)(ipv4) + } catch (cause) { + throw new IPv4Error({ + message: "Failed to encode IPv4 to bytes", + cause + }) + } +} + +/** + * Encode IPv4 to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (ipv4: IPv4): string => { + try { + return Schema.encodeSync(FromHex)(ipv4) + } catch (cause) { + throw new IPv4Error({ + message: "Failed to encode IPv4 to hex", + cause + }) + } +} + +// ============================================================================ +// Either Namespace +// ============================================================================ + +/** + * Either-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category either + */ +export namespace Either { + /** + * Parse IPv4 from bytes with Either error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): E.Either => + E.mapLeft( + Schema.decodeEither(FromBytes)(bytes), + (cause) => + new IPv4Error({ + message: "Failed to parse IPv4 from bytes", + cause + }) + ) + + /** + * Parse IPv4 from hex string with Either error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): E.Either => + E.mapLeft( + Schema.decodeEither(FromHex)(hex), + (cause) => + new IPv4Error({ + message: "Failed to parse IPv4 from hex", + cause + }) + ) + + /** + * Encode IPv4 to bytes with Either error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (ipv4: IPv4): E.Either => + E.mapLeft( + Schema.encodeEither(FromBytes)(ipv4), + (cause) => + new IPv4Error({ + message: "Failed to encode IPv4 to bytes", + cause + }) + ) + + /** + * Encode IPv4 to hex string with Either error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (ipv4: IPv4): E.Either => + E.mapLeft( + Schema.encodeEither(FromHex)(ipv4), + (cause) => + new IPv4Error({ + message: "Failed to encode IPv4 to hex", + cause + }) + ) +} diff --git a/packages/evolution/src/IPv6.ts b/packages/evolution/src/IPv6.ts index 7b97f188..04bb0f95 100644 --- a/packages/evolution/src/IPv6.ts +++ b/packages/evolution/src/IPv6.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes16 from "./Bytes16.js" -import { createEncoders } from "./Codec.js" /** * Error class for IPv6 related operations. @@ -21,7 +20,7 @@ export class IPv6Error extends Data.TaggedError("IPv6Error")<{ * @since 2.0.0 * @category schemas */ -export const IPv6 = pipe(Bytes16.HexSchema, Schema.brand("IPv6")).annotations({ +export const IPv6 = Bytes16.HexSchema.pipe(Schema.brand("IPv6")).annotations({ identifier: "IPv6" }) @@ -50,26 +49,140 @@ export const FromHex = Schema.compose( export const equals = (a: IPv6, b: IPv6): boolean => a === b /** - * Generate a random IPv6. + * Check if the given value is a valid IPv6 * * @since 2.0.0 - * @category generators + * @category predicates */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes16.BYTES_LENGTH, - maxLength: Bytes16.BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const isIPv6 = Schema.is(IPv6) /** - * Codec utilities for IPv6 encoding and decoding operations. + * FastCheck arbitrary for generating random IPv6 instances. * * @since 2.0.0 - * @category encoding/decoding + * @category arbitrary */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - IPv6Error -) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes16.HEX_LENGTH, + maxLength: Bytes16.HEX_LENGTH +}).map((hex) => hex as IPv6) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse IPv6 from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): IPv6 => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse IPv6 from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): IPv6 => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode IPv6 to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (ipv6: IPv6): Uint8Array => + Eff.runSync(Effect.toBytes(ipv6)) + +/** + * Encode IPv6 to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (ipv6: IPv6): string => + Eff.runSync(Effect.toHex(ipv6)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse IPv6 from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new IPv6Error({ + message: "Failed to parse IPv6 from bytes", + cause + }) + ) + ) + + /** + * Parse IPv6 from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new IPv6Error({ + message: "Failed to parse IPv6 from hex", + cause + }) + ) + ) + + /** + * Encode IPv6 to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (ipv6: IPv6): Eff.Effect => + Schema.encode(FromBytes)(ipv6).pipe( + Eff.mapError( + (cause) => + new IPv6Error({ + message: "Failed to encode IPv6 to bytes", + cause + }) + ) + ) + + /** + * Encode IPv6 to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (ipv6: IPv6): Eff.Effect => + Schema.encode(FromHex)(ipv6).pipe( + Eff.mapError( + (cause) => + new IPv6Error({ + message: "Failed to encode IPv6 to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/KESVkey.ts b/packages/evolution/src/KESVkey.ts index d457799a..4a9ca6f8 100644 --- a/packages/evolution/src/KESVkey.ts +++ b/packages/evolution/src/KESVkey.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" /** * Error class for KESVkey related operations. @@ -22,7 +21,7 @@ export class KESVkeyError extends Data.TaggedError("KESVkeyError")<{ * @since 2.0.0 * @category schemas */ -export const KESVkey = pipe(Bytes32.HexSchema, Schema.brand("KESVkey")).annotations({ +export const KESVkey = Bytes32.HexSchema.pipe(Schema.brand("KESVkey")).annotations({ identifier: "KESVkey" }) @@ -51,26 +50,140 @@ export const FromHex = Schema.compose( export const equals = (a: KESVkey, b: KESVkey): boolean => a === b /** - * Generate a random KESVkey. + * Check if the given value is a valid KESVkey * * @since 2.0.0 - * @category generators + * @category predicates */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes32.Bytes32_BYTES_LENGTH, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const isKESVkey = Schema.is(KESVkey) /** - * Codec utilities for KESVkey encoding and decoding operations. + * FastCheck arbitrary for generating random KESVkey instances. * * @since 2.0.0 - * @category encoding/decoding + * @category arbitrary */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - KESVkeyError -) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes32.HEX_LENGTH, + maxLength: Bytes32.HEX_LENGTH +}).map((hex) => hex as KESVkey) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse KESVkey from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): KESVkey => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse KESVkey from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): KESVkey => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode KESVkey to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (kesVkey: KESVkey): Uint8Array => + Eff.runSync(Effect.toBytes(kesVkey)) + +/** + * Encode KESVkey to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (kesVkey: KESVkey): string => + Eff.runSync(Effect.toHex(kesVkey)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse KESVkey from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new KESVkeyError({ + message: "Failed to parse KESVkey from bytes", + cause + }) + ) + ) + + /** + * Parse KESVkey from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new KESVkeyError({ + message: "Failed to parse KESVkey from hex", + cause + }) + ) + ) + + /** + * Encode KESVkey to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (kesVkey: KESVkey): Eff.Effect => + Schema.encode(FromBytes)(kesVkey).pipe( + Eff.mapError( + (cause) => + new KESVkeyError({ + message: "Failed to encode KESVkey to bytes", + cause + }) + ) + ) + + /** + * Encode KESVkey to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (kesVkey: KESVkey): Eff.Effect => + Schema.encode(FromHex)(kesVkey).pipe( + Eff.mapError( + (cause) => + new KESVkeyError({ + message: "Failed to encode KESVkey to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/KesSignature.ts b/packages/evolution/src/KesSignature.ts index 3ae22f51..8ed479e1 100644 --- a/packages/evolution/src/KesSignature.ts +++ b/packages/evolution/src/KesSignature.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes448 from "./Bytes448.js" -import { createEncoders } from "./Codec.js" /** * Error class for KesSignature related operations. @@ -22,7 +21,7 @@ export class KesSignatureError extends Data.TaggedError("KesSignatureError")<{ * @since 2.0.0 * @category schemas */ -export const KesSignature = pipe(Bytes448.HexSchema, Schema.brand("KesSignature")).annotations({ +export const KesSignature = Bytes448.HexSchema.pipe(Schema.brand("KesSignature")).annotations({ identifier: "KesSignature" }) @@ -51,26 +50,140 @@ export const FromHex = Schema.compose( export const equals = (a: KesSignature, b: KesSignature): boolean => a === b /** - * Generate a random KesSignature. + * Check if the given value is a valid KesSignature * * @since 2.0.0 - * @category generators + * @category predicates */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes448.BYTES_LENGTH, - maxLength: Bytes448.BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const isKesSignature = Schema.is(KesSignature) /** - * Codec utilities for KesSignature encoding and decoding operations. + * FastCheck arbitrary for generating random KesSignature instances. * * @since 2.0.0 - * @category encoding/decoding + * @category arbitrary */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - KesSignatureError -) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes448.HEX_LENGTH, + maxLength: Bytes448.HEX_LENGTH +}).map((hex) => hex as KesSignature) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse KesSignature from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): KesSignature => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse KesSignature from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): KesSignature => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode KesSignature to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (kesSignature: KesSignature): Uint8Array => + Eff.runSync(Effect.toBytes(kesSignature)) + +/** + * Encode KesSignature to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (kesSignature: KesSignature): string => + Eff.runSync(Effect.toHex(kesSignature)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse KesSignature from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new KesSignatureError({ + message: "Failed to parse KesSignature from bytes", + cause + }) + ) + ) + + /** + * Parse KesSignature from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new KesSignatureError({ + message: "Failed to parse KesSignature from hex", + cause + }) + ) + ) + + /** + * Encode KesSignature to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (kesSignature: KesSignature): Eff.Effect => + Schema.encode(FromBytes)(kesSignature).pipe( + Eff.mapError( + (cause) => + new KesSignatureError({ + message: "Failed to encode KesSignature to bytes", + cause + }) + ) + ) + + /** + * Encode KesSignature to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (kesSignature: KesSignature): Eff.Effect => + Schema.encode(FromHex)(kesSignature).pipe( + Eff.mapError( + (cause) => + new KesSignatureError({ + message: "Failed to encode KesSignature to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/KeyHash.ts b/packages/evolution/src/KeyHash.ts index f525c808..c7171de8 100644 --- a/packages/evolution/src/KeyHash.ts +++ b/packages/evolution/src/KeyHash.ts @@ -1,7 +1,10 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { blake2b } from "@noble/hashes/blake2" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" +import sodium from "libsodium-wrappers-sumo" -import { createEncoders } from "./Codec.js" import * as Hash28 from "./Hash28.js" +import * as PrivateKey from "./PrivateKey.js" +import * as VKey from "./VKey.js" /** * Error class for KeyHash related operations. @@ -22,8 +25,10 @@ export class KeyHashError extends Data.TaggedError("KeyHashError")<{ * @since 2.0.0 * @category schemas */ -export const KeyHash = pipe(Hash28.HexSchema, Schema.brand("KeyHash")).annotations({ - identifier: "KeyHash" +export const KeyHash = Hash28.HexSchema.pipe(Schema.brand("KeyHash")).annotations({ + identifier: "KeyHash", + title: "Verification Key Hash", + description: "A 28-byte verification key hash" }) export type KeyHash = typeof KeyHash.Type @@ -32,15 +37,21 @@ export const FromBytes = Schema.compose( Hash28.FromBytes, // Uint8Array -> hex string KeyHash // hex string -> KeyHash ).annotations({ - identifier: "KeyHash.FromBytes" + identifier: "KeyHash.FromBytes", + title: "KeyHash from Bytes", + description: "Transforms raw bytes (Uint8Array) to KeyHash hex string", + message: () => "Invalid key hash bytes - must be exactly 28 bytes" }) -export const FromHex = Schema.compose( - Hash28.HexSchema, // string -> hex string - KeyHash // hex string -> KeyHash -).annotations({ - identifier: "KeyHash.FromHex" -}) +export const FromHex = KeyHash + +/** + * Smart constructor for KeyHash that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = KeyHash.make /** * Check if two KeyHash instances are equal. @@ -50,27 +61,231 @@ export const FromHex = Schema.compose( */ export const equals = (a: KeyHash, b: KeyHash): boolean => a === b +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse a KeyHash from raw bytes. + * Expects exactly 28 bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): KeyHash => Eff.runSync(Effect.fromBytes(bytes)) + /** - * Generate a random KeyHash. + * Parse a KeyHash from a hex string. + * Expects exactly 56 hex characters (28 bytes). * * @since 2.0.0 - * @category generators + * @category parsing */ -export const generator = FastCheck.uint8Array({ - minLength: Hash28.HASH28_BYTES_LENGTH, - maxLength: Hash28.HASH28_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const fromHex = (hex: string): KeyHash => Eff.runSync(Effect.fromHex(hex)) + +/** + * FastCheck arbitrary for generating random KeyHash instances. + * Used for property-based testing to generate valid test data. + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary: FastCheck.Arbitrary = FastCheck + .uint8Array({ minLength: 28, maxLength: 28 }) + .map(fromBytes) + +// ============================================================================ +// Encoding Functions +// ============================================================================ /** - * Codec utilities for KeyHash encoding and decoding operations. + * Convert a KeyHash to raw bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category encoding */ -export const Codec = createEncoders( - { - bytes: FromBytes, - string: FromHex - }, - KeyHashError -) +export const toBytes = (keyHash: KeyHash): Uint8Array => Eff.runSync(Effect.toBytes(keyHash)) + +/** + * Convert a KeyHash to a hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (keyHash: KeyHash): string => keyHash // Already a hex string + +// ============================================================================ +// Cryptographic Operations +// ============================================================================ + +/** + * Create a KeyHash from a PrivateKey (sync version that throws KeyHashError). + * All errors are normalized to KeyHashError with contextual information. + * + * @since 2.0.0 + * @category cryptography + */ +export const fromPrivateKey = (privateKey: PrivateKey.PrivateKey): KeyHash => Eff.runSync(Effect.fromPrivateKey(privateKey)) + +/** + * Create a KeyHash from a VKey (sync version that throws KeyHashError). + * All errors are normalized to KeyHashError with contextual information. + * + * @since 2.0.0 + * @category cryptography + */ +export const fromVKey = (vkey: VKey.VKey): KeyHash => Eff.runSync(Effect.fromVKey(vkey)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse a KeyHash from raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new KeyHashError({ + message: "Failed to parse KeyHash from bytes", + cause + }) + ) + ) + + /** + * Parse a KeyHash from a hex string using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new KeyHashError({ + message: "Failed to parse KeyHash from hex", + cause + }) + ) + ) + + /** + * Convert a KeyHash to raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (keyHash: KeyHash): Eff.Effect => + Schema.encode(FromBytes)(keyHash).pipe( + Eff.mapError( + (cause) => + new KeyHashError({ + message: "Failed to encode KeyHash to bytes", + cause + }) + ) + ) + + /** + * Create a KeyHash from a PrivateKey using Effect error handling. + * + * @since 2.0.0 + * @category cryptography + */ + export const fromPrivateKey = (privateKey: PrivateKey.PrivateKey): Eff.Effect => + Eff.gen(function* () { + const privateKeyBytes = yield* Eff.mapError( + Schema.encode(PrivateKey.FromBytes)(privateKey), + (cause) => + new KeyHashError({ + message: "Failed to encode private key to bytes", + cause + }) + ) + + const publicKeyBytes = yield* Eff.try({ + try: () => { + if (privateKeyBytes.length === 64) { + // CML-compatible extended private key: use first 32 bytes as scalar + const scalar = privateKeyBytes.slice(0, 32) + return sodium.crypto_scalarmult_ed25519_base_noclamp(scalar) + } else { + // Standard 32-byte Ed25519 private key using sodium + return sodium.crypto_sign_seed_keypair(privateKeyBytes).publicKey + } + }, + catch: (cause) => + new KeyHashError({ + message: "Failed to derive public key from private key", + cause + }) + }) + + const keyHashBytes = yield* Eff.try({ + try: () => blake2b(publicKeyBytes, { dkLen: 28 }), + catch: (cause) => + new KeyHashError({ + message: "Failed to hash public key", + cause + }) + }) + + return yield* Eff.mapError( + Schema.decode(FromBytes)(keyHashBytes), + (cause) => + new KeyHashError({ + message: "Failed to create KeyHash from hash bytes", + cause + }) + ) + }) + + /** + * Create a KeyHash from a VKey using Effect error handling. + * + * @since 2.0.0 + * @category cryptography + */ + export const fromVKey = (vkey: VKey.VKey): Eff.Effect => + Eff.gen(function* () { + const publicKeyBytes = yield* Eff.mapError( + Schema.encode(VKey.FromBytes)(vkey), + (cause) => + new KeyHashError({ + message: "Failed to encode VKey to bytes", + cause + }) + ) + + const keyHashBytes = yield* Eff.try({ + try: () => blake2b(publicKeyBytes, { dkLen: 28 }), + catch: (cause) => + new KeyHashError({ + message: "Failed to hash public key", + cause + }) + }) + + return yield* Eff.mapError( + Schema.decode(FromBytes)(keyHashBytes), + (cause) => + new KeyHashError({ + message: "Failed to create KeyHash from hash bytes", + cause + }) + ) + }) +} diff --git a/packages/evolution/src/Mint.ts b/packages/evolution/src/Mint.ts index 31665176..fd483d3d 100644 --- a/packages/evolution/src/Mint.ts +++ b/packages/evolution/src/Mint.ts @@ -1,4 +1,4 @@ -import { Data, Effect, Equal, FastCheck, ParseResult, Pretty, Schema } from "effect" +import { Data, Effect as Eff, Equal, FastCheck, ParseResult, Schema } from "effect" import * as AssetName from "./AssetName.js" import * as Bytes from "./Bytes.js" @@ -15,7 +15,7 @@ import * as PolicyId from "./PolicyId.js" */ export class MintError extends Data.TaggedError("MintError")<{ message?: string - reason?: "InvalidStructure" | "EmptyAssetMap" | "ZeroAmount" | "DuplicateAsset" + cause?: unknown }> {} /** @@ -29,13 +29,10 @@ export class MintError extends Data.TaggedError("MintError")<{ */ export const AssetMap = Schema.MapFromSelf({ key: AssetName.AssetName, - value: NonZeroInt64.NonZeroInt64Schema + value: NonZeroInt64.NonZeroInt64 +}).annotations({ + identifier: "AssetMap" }) - .pipe(Schema.filter((map) => map.size > 0)) - .annotations({ - message: () => "Asset map cannot be empty", - identifier: "AssetMap" - }) export type AssetMap = typeof AssetMap.Type @@ -56,10 +53,11 @@ export const Mint = Schema.MapFromSelf({ key: PolicyId.PolicyId, value: AssetMap }) - .pipe(Schema.filter((map) => map.size > 0)) + .pipe(Schema.brand("Mint")) .annotations({ - message: () => "Mint cannot be empty", - identifier: "Mint" + identifier: "Mint", + title: "Token Mint Operations", + description: "A collection of token minting/burning operations grouped by policy ID" }) /** @@ -72,8 +70,13 @@ export const Mint = Schema.MapFromSelf({ */ export type Mint = typeof Mint.Type -type PrettyPrint = (self: Mint) => string -export const prettyPrint: PrettyPrint = Pretty.make(Mint) +/** + * Check if a value is a valid Mint. + * + * @since 2.0.0 + * @category predicates + */ +export const is = Schema.is(Mint) /** * Create empty Mint. @@ -81,7 +84,7 @@ export const prettyPrint: PrettyPrint = Pretty.make(Mint) * @since 2.0.0 * @category constructors */ -export const empty = (): Mint => new Map() +export const empty = (): Mint => new Map() as Mint /** * Create Mint from a single policy and asset entry. @@ -95,24 +98,27 @@ export const singleton = ( amount: NonZeroInt64.NonZeroInt64 ): Mint => { const assetMap = new Map([[assetName, amount]]) - return new Map([[policyId, assetMap]]) + return new Map([[policyId, assetMap]]) as Mint } /** - * Add or update an asset in the Mint. + * Create Mint from entries array. * * @since 2.0.0 - * @category transformation - */ -/** - * Helper function to create Mint from entries array + * @category constructors */ export const fromEntries = ( entries: Array<[PolicyId.PolicyId, Array<[AssetName.AssetName, NonZeroInt64.NonZeroInt64]>]> ): Mint => { - return new Map(entries.map(([policyId, assetEntries]) => [policyId, new Map(assetEntries)])) + return new Map(entries.map(([policyId, assetEntries]) => [policyId, new Map(assetEntries)])) as Mint } +/** + * Add or update an asset in the Mint. + * + * @since 2.0.0 + * @category transformation + */ export const insert = ( mint: Mint, policyId: PolicyId.PolicyId, @@ -126,7 +132,7 @@ export const insert = ( const result = new Map(mint) result.set(policyId, assetMap) - return result + return result as Mint } /** @@ -138,7 +144,7 @@ export const insert = ( export const removePolicy = (mint: Mint, policyId: PolicyId.PolicyId): Mint => { const result = new Map(mint) result.delete(policyId) - return result + return result as Mint } export const removeAsset = (mint: Mint, policyId: PolicyId.PolicyId, assetName: AssetName.AssetName): Mint => { @@ -153,12 +159,12 @@ export const removeAsset = (mint: Mint, policyId: PolicyId.PolicyId, assetName: // If no assets left, remove the policyId entry const result = new Map(mint) result.delete(policyId) - return result + return result as Mint } const result = new Map(mint) result.set(policyId, updatedAssets) - return result + return result as Mint } /** @@ -212,19 +218,13 @@ export const policyCount = (mint: Mint): number => mint.size */ export const equals = (self: Mint, that: Mint): boolean => Equal.equals(self, that) -/** - * FastCheck generator for Mint instances. - * - * @since 2.0.0 - * @category generators - */ -export const generator = FastCheck.array( - FastCheck.tuple( - PolicyId.generator, - FastCheck.array(FastCheck.tuple(AssetName.generator, NonZeroInt64.generator), { minLength: 1, maxLength: 5 }) - ), - { minLength: 0, maxLength: 5 } -).map((entries) => fromEntries(entries)) +export const CDDLSchema = Schema.MapFromSelf({ + key: CBOR.ByteArray, // Policy ID as Uint8Array (28 bytes) + value: Schema.MapFromSelf({ + key: CBOR.ByteArray, // Asset name as Uint8Array (variable length) + value: CBOR.Integer // Amount as number (will be converted to NonZeroInt64) + }) +}) /** * CDDL schema for Mint as map structure. @@ -240,91 +240,217 @@ export const generator = FastCheck.array( * @since 2.0.0 * @category schemas */ -export const MintCDDLSchema = Schema.transformOrFail( - Schema.MapFromSelf({ - key: CBOR.ByteArray, // Policy ID as Uint8Array (28 bytes) - value: Schema.MapFromSelf({ - key: CBOR.ByteArray, // Asset name as Uint8Array (variable length) - value: CBOR.Integer // Amount as number (will be converted to NonZeroInt64) - }) - }), - Schema.typeSchema(Mint), - { - strict: true, - encode: (toA) => - Effect.gen(function* () { - // Convert Mint to raw Map data for CBOR encoding - const outerMap = new Map>() - - for (const [policyId, assetMap] of toA.entries()) { - const policyIdBytes = yield* ParseResult.encode(PolicyId.FromBytes)(policyId) - const innerMap = new Map() - - for (const [assetName, amount] of assetMap.entries()) { - const assetNameBytes = yield* ParseResult.encode(AssetName.FromBytes)(assetName) - innerMap.set(assetNameBytes, amount) - } - - outerMap.set(policyIdBytes, innerMap) +export const MintCDDLSchema = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Mint), { + strict: true, + encode: (toA) => + Eff.gen(function* () { + // Convert Mint to raw Map data for CBOR encoding + const outerMap = new Map>() + + for (const [policyId, assetMap] of toA.entries()) { + const policyIdBytes = yield* ParseResult.encode(PolicyId.FromBytes)(policyId) + const innerMap = new Map() + + for (const [assetName, amount] of assetMap.entries()) { + const assetNameBytes = yield* ParseResult.encode(AssetName.FromBytes)(assetName) + innerMap.set(assetNameBytes, amount) } - return outerMap - }), + outerMap.set(policyIdBytes, innerMap) + } - decode: (fromA) => - Effect.gen(function* () { - const mint = new Map() + return outerMap + }), - for (const [policyIdBytes, assetMapCddl] of fromA.entries()) { - const policyId = yield* ParseResult.decode(PolicyId.FromBytes)(policyIdBytes) + decode: (fromA) => + Eff.gen(function* () { + const mint = empty() - const assetMap = new Map() - for (const [assetNameBytes, amount] of assetMapCddl.entries()) { - const assetName = yield* ParseResult.decode(AssetName.FromBytes)(assetNameBytes) - const nonZeroAmount = yield* ParseResult.decode(NonZeroInt64.NonZeroInt64Schema)(amount) + for (const [policyIdBytes, assetMapCddl] of fromA.entries()) { + const policyId = yield* ParseResult.decode(PolicyId.FromBytes)(policyIdBytes) - assetMap.set(assetName, nonZeroAmount) - } + const assetMap = new Map() + for (const [assetNameBytes, amount] of assetMapCddl.entries()) { + const assetName = yield* ParseResult.decode(AssetName.FromBytes)(assetNameBytes) + const nonZeroAmount = yield* ParseResult.decode(NonZeroInt64.NonZeroInt64)(amount) - mint.set(policyId, assetMap) + assetMap.set(assetName, nonZeroAmount) } - return mint - }) - } -) + mint.set(policyId, assetMap) + } + + return mint + }) +}) /** * CBOR bytes transformation schema for Mint. - * Transforms between Uint8Array and Mint using CBOR encoding. + * Transforms between CBOR bytes and Mint using CBOR encoding. * * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR MintCDDLSchema // CBOR → Mint - ) + ).annotations({ + identifier: "Mint.FromCBORBytes", + title: "Mint from CBOR Bytes", + description: "Transforms CBOR bytes to Mint" + }) /** * CBOR hex transformation schema for Mint. - * Transforms between hex string and Mint using CBOR encoding. + * Transforms between CBOR hex string and Mint using CBOR encoding. * * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → Mint - ) - -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - MintError - ) + FromCBORBytes(options) // Uint8Array → Mint + ).annotations({ + identifier: "Mint.FromCBORHex", + title: "Mint from CBOR Hex", + description: "Transforms CBOR hex string to Mint" + }) + +/** + * FastCheck arbitrary for generating random Mint instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.array( + FastCheck.tuple( + PolicyId.arbitrary, + FastCheck.array(FastCheck.tuple(AssetName.arbitrary, NonZeroInt64.arbitrary.map(NonZeroInt64.make)), { + minLength: 1, + maxLength: 5 + }) + ), + { minLength: 0, maxLength: 5 } +).map((entries) => fromEntries(entries)) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse Mint from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Mint => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse Mint from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Mint => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Encode Mint to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (mint: Mint, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(mint, options)) + +/** + * Encode Mint to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (mint: Mint, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(mint, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Mint from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new MintError({ + message: "Failed to parse Mint from CBOR bytes", + cause + }) + ) + ) + + /** + * Parse Mint from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new MintError({ + message: "Failed to parse Mint from CBOR hex", + cause + }) + ) + ) + + /** + * Encode Mint to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (mint: Mint, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORBytes(options))(mint).pipe( + Eff.mapError( + (cause) => + new MintError({ + message: "Failed to encode Mint to CBOR bytes", + cause + }) + ) + ) + + /** + * Encode Mint to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (mint: Mint, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORHex(options))(mint).pipe( + Eff.mapError( + (cause) => + new MintError({ + message: "Failed to encode Mint to CBOR hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/MultiAsset.ts b/packages/evolution/src/MultiAsset.ts index cb747315..1c57d0fe 100644 --- a/packages/evolution/src/MultiAsset.ts +++ b/packages/evolution/src/MultiAsset.ts @@ -1,4 +1,4 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as AssetName from "./AssetName.js" import * as Bytes from "./Bytes.js" @@ -24,7 +24,7 @@ export class MultiAssetError extends Data.TaggedError("MultiAssetError")<{ * @since 2.0.0 * @category schemas */ -export const AssetMapSchema = Schema.Map({ +export const AssetMap = Schema.MapFromSelf({ key: AssetName.AssetName, value: PositiveCoin.PositiveCoinSchema }) @@ -40,7 +40,7 @@ export const AssetMapSchema = Schema.Map({ * @since 2.0.0 * @category model */ -export type AssetMap = typeof AssetMapSchema.Type +export type AssetMap = typeof AssetMap.Type /** * Schema for MultiAsset representing native assets. @@ -53,14 +53,17 @@ export type AssetMap = typeof AssetMapSchema.Type * @since 2.0.0 * @category schemas */ -export const MultiAssetSchema = Schema.MapFromSelf({ +export const MultiAsset = Schema.MapFromSelf({ key: PolicyId.PolicyId, - value: AssetMapSchema + value: AssetMap }) .pipe(Schema.filter((map) => map.size > 0)) + .pipe(Schema.brand("MultiAsset")) .annotations({ message: () => "MultiAsset cannot be empty", - identifier: "MultiAsset" + identifier: "MultiAsset", + title: "Multi-Asset Collection", + description: "A collection of native assets grouped by policy ID with positive amounts" }) /** @@ -71,11 +74,19 @@ export const MultiAssetSchema = Schema.MapFromSelf({ * @since 2.0.0 * @category model */ -export type MultiAsset = typeof MultiAssetSchema.Type +export interface MultiAsset extends Schema.Schema.Type {} /** - * Create an empty MultiAsset (note: this will fail validation as MultiAsset cannot be empty). - * Use this only as a starting point for building a MultiAsset. + * Smart constructor for MultiAsset that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = Schema.decodeSync(MultiAsset) + +/** + * Create an empty Map for building MultiAssets (note: empty maps will fail validation). + * Use this only as a starting point for building a MultiAsset with add operations. * * @since 2.0.0 * @category constructors @@ -94,7 +105,7 @@ export const singleton = ( amount: PositiveCoin.PositiveCoin ): MultiAsset => { const assetMap = new Map([[assetName, amount]]) - return new Map([[policyId, assetMap]]) + return make(new Map([[policyId, assetMap]])) } /** @@ -120,12 +131,12 @@ export const addAsset = ( const result = new Map(multiAsset) result.set(policyId, updatedAssetMap) - return result + return make(result) } else { const newAssetMap = new Map([[assetName, amount]]) const result = new Map(multiAsset) result.set(policyId, newAssetMap) - return result + return make(result) } } @@ -139,9 +150,9 @@ export const getAsset = (multiAsset: MultiAsset, policyId: PolicyId.PolicyId, as const assetMap = multiAsset.get(policyId) if (assetMap !== undefined) { const amount = assetMap.get(assetName) - return amount !== undefined ? { _tag: "Some" as const, value: amount } : { _tag: "None" as const } + return amount !== undefined ? amount : undefined } - return { _tag: "None" as const } + return undefined } /** @@ -156,7 +167,7 @@ export const hasAsset = ( assetName: AssetName.AssetName ): boolean => { const result = getAsset(multiAsset, policyId, assetName) - return result._tag === "Some" + return result !== undefined } /** @@ -165,7 +176,7 @@ export const hasAsset = ( * @since 2.0.0 * @category transformation */ -export const getPolicyIds = (multiAsset: MultiAsset) => multiAsset.keys() +export const getPolicyIds = (multiAsset: MultiAsset): Array => Array.from(multiAsset.keys()) /** * Get all assets for a specific policy ID. @@ -175,7 +186,7 @@ export const getPolicyIds = (multiAsset: MultiAsset) => multiAsset.keys() */ export const getAssetsByPolicy = (multiAsset: MultiAsset, policyId: PolicyId.PolicyId) => { const assetMap = multiAsset.get(policyId) - return assetMap !== undefined ? { _tag: "Some" as const, value: assetMap } : { _tag: "None" as const } + return assetMap !== undefined ? Array.from(assetMap.entries()) : [] } /** @@ -210,23 +221,23 @@ export const equals = (a: MultiAsset, b: MultiAsset): boolean => * @since 2.0.0 * @category predicates */ -export const is = (value: unknown): value is MultiAsset => Schema.is(MultiAssetSchema)(value) +export const is = (value: unknown): value is MultiAsset => Schema.is(MultiAsset)(value) /** - * Generate a random MultiAsset. + * Change generator to arbitrary and rename CBOR schemas. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.array( +export const arbitrary = FastCheck.array( FastCheck.tuple( - PolicyId.generator, - FastCheck.array(FastCheck.tuple(AssetName.generator, PositiveCoin.generator), { minLength: 1, maxLength: 5 }).map( - (assets) => new Map(assets) + PolicyId.arbitrary, + FastCheck.array(FastCheck.tuple(AssetName.arbitrary, PositiveCoin.arbitrary), { minLength: 1, maxLength: 5 }).map( + (tokens) => new Map(tokens) ) ), - { minLength: 1, maxLength: 3 } -).map((policies) => new Map(policies)) + { minLength: 1, maxLength: 5 } +).map((entries) => make(new Map(entries))) /** * CDDL schema for MultiAsset. @@ -246,11 +257,11 @@ export const MultiAssetCDDLSchema = Schema.transformOrFail( value: CBOR.Integer }) }), - Schema.typeSchema(MultiAssetSchema), + Schema.typeSchema(MultiAsset), { strict: true, encode: (toI, _, __, toA) => - Effect.gen(function* () { + Eff.gen(function* () { // Convert MultiAsset to raw Map data for CBOR encoding const outerMap = new Map>() @@ -270,7 +281,7 @@ export const MultiAssetCDDLSchema = Schema.transformOrFail( }), decode: (fromA) => - Effect.gen(function* () { + Eff.gen(function* () { const result = new Map() for (const [policyIdBytes, assetMapCddl] of fromA.entries()) { @@ -286,43 +297,85 @@ export const MultiAssetCDDLSchema = Schema.transformOrFail( result.set(policyId, assetMap) } - return result + return yield* ParseResult.decode(MultiAsset)(result) }) } ) /** * CBOR bytes transformation schema for MultiAsset. + * Transforms between CBOR bytes and MultiAsset using CBOR encoding. * * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR MultiAssetCDDLSchema // CBOR → MultiAsset - ) + ).annotations({ + identifier: "MultiAsset.FromCBORBytes", + title: "MultiAsset from CBOR Bytes", + description: "Transforms CBOR bytes to MultiAsset" + }) /** * CBOR hex transformation schema for MultiAsset. + * Transforms between CBOR hex string and MultiAsset using CBOR encoding. * * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → MultiAsset - ) - -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - MultiAssetError - ) + FromCBORBytes(options) // Uint8Array → MultiAsset + ).annotations({ + identifier: "MultiAsset.FromCBORHex", + title: "MultiAsset from CBOR Hex", + description: "Transforms CBOR hex string to MultiAsset" + }) + +/** + * Root Functions + * ============================================================================ + */ + +/** + * Parse MultiAsset from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): MultiAsset => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse MultiAsset from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): MultiAsset => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Encode MultiAsset to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (multiAsset: MultiAsset, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(multiAsset, options)) + +/** + * Encode MultiAsset to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (multiAsset: MultiAsset, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(multiAsset, options)) /** * Merge two MultiAsset instances, combining amounts for assets that exist in both. @@ -396,5 +449,93 @@ export const subtract = (a: MultiAsset, b: MultiAsset): MultiAsset => { }) } - return result + return make(result) } + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse MultiAsset from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = ( + bytes: Uint8Array, + options?: CBOR.CodecOptions + ): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new MultiAssetError({ + message: "Failed to parse MultiAsset from CBOR bytes", + cause + }) + ) + ) + + /** + * Parse MultiAsset from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new MultiAssetError({ + message: "Failed to parse MultiAsset from CBOR hex", + cause + }) + ) + ) + + /** + * Encode MultiAsset to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = ( + multiAsset: MultiAsset, + options?: CBOR.CodecOptions + ): Eff.Effect => + Schema.encode(FromCBORBytes(options))(multiAsset).pipe( + Eff.mapError( + (cause) => + new MultiAssetError({ + message: "Failed to encode MultiAsset to CBOR bytes", + cause + }) + ) + ) + + /** + * Encode MultiAsset to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (multiAsset: MultiAsset, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORHex(options))(multiAsset).pipe( + Eff.mapError( + (cause) => + new MultiAssetError({ + message: "Failed to encode MultiAsset to CBOR hex", + cause + }) + ) + ) +} + +// ============================================================================ diff --git a/packages/evolution/src/MultiHostName.ts b/packages/evolution/src/MultiHostName.ts index 0318a177..684c5e19 100644 --- a/packages/evolution/src/MultiHostName.ts +++ b/packages/evolution/src/MultiHostName.ts @@ -1,8 +1,7 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as DnsName from "./DnsName.js" /** @@ -43,17 +42,20 @@ export const FromCDDL = Schema.transformOrFail( { strict: true, encode: (toA) => - Effect.gen(function* () { + Eff.gen(function* () { const dnsName = yield* ParseResult.encode(DnsName.DnsName)(toA.dnsName) - return yield* Effect.succeed([2n, dnsName] as const) + return yield* Eff.succeed([2n, dnsName] as const) }), decode: ([, dnsNameValue]) => - Effect.gen(function* () { + Eff.gen(function* () { const dnsName = yield* ParseResult.decode(DnsName.DnsName)(dnsNameValue) - return yield* Effect.succeed(new MultiHostName({ dnsName })) + return yield* Eff.succeed(new MultiHostName({ dnsName })) }) } -) +).annotations({ + identifier: "MultiHostName.FromCDDL", + description: "Transforms CBOR structure to MultiHostName" +}) /** * CBOR bytes transformation schema for MultiHostName. @@ -61,11 +63,14 @@ export const FromCDDL = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → MultiHostName - ) + ).annotations({ + identifier: "MultiHostName.FromCBORBytes", + description: "Transforms CBOR bytes to MultiHostName" + }) /** * CBOR hex transformation schema for MultiHostName. @@ -73,11 +78,14 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → MultiHostName - ) + FromCBORBytes(options) // Uint8Array → MultiHostName + ).annotations({ + identifier: "MultiHostName.FromCBORHex", + description: "Transforms CBOR hex string to MultiHostName" + }) /** * Create a MultiHostName instance. @@ -96,20 +104,103 @@ export const make = (dnsName: DnsName.DnsName): MultiHostName => new MultiHostNa export const equals = (self: MultiHostName, that: MultiHostName): boolean => DnsName.equals(self.dnsName, that.dnsName) /** - * FastCheck generator for MultiHostName instances. + * FastCheck arbitrary for MultiHostName instances. * * @since 2.0.0 - * @category generators + * @category testing */ -export const generator = FastCheck.record({ - dnsName: DnsName.generator +export const arbitrary = FastCheck.record({ + dnsName: DnsName.arbitrary }).map((props) => new MultiHostName(props)) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - MultiHostNameError - ) +/** + * Effect namespace for MultiHostName operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert CBOR bytes to MultiHostName using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (cause) => new MultiHostNameError({ message: "Failed to decode from CBOR bytes", cause }) + ) + + /** + * Convert CBOR hex string to MultiHostName using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (cause) => new MultiHostNameError({ message: "Failed to decode from CBOR hex", cause }) + ) + + /** + * Convert MultiHostName to CBOR bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORBytes = (hostName: MultiHostName, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(hostName), + (cause) => new MultiHostNameError({ message: "Failed to encode to CBOR bytes", cause }) + ) + + /** + * Convert MultiHostName to CBOR hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORHex = (hostName: MultiHostName, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(hostName), + (cause) => new MultiHostNameError({ message: "Failed to encode to CBOR hex", cause }) + ) +} + +/** + * Convert CBOR bytes to MultiHostName (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): MultiHostName => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Convert CBOR hex string to MultiHostName (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): MultiHostName => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Convert MultiHostName to CBOR bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORBytes = (hostName: MultiHostName, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(hostName, options)) + +/** + * Convert MultiHostName to CBOR hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORHex = (hostName: MultiHostName, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(hostName, options)) diff --git a/packages/evolution/src/NativeScripts.ts b/packages/evolution/src/NativeScripts.ts index 38b63dca..d32004c4 100644 --- a/packages/evolution/src/NativeScripts.ts +++ b/packages/evolution/src/NativeScripts.ts @@ -1,165 +1,103 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, ParseResult, Schema } from "effect" import type { ParseIssue } from "effect/ParseResult" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import { Bytes } from "./index.js" -import * as KeyHash from "./KeyHash.js" -import * as NativeScriptJSON from "./NativeScriptJSON.js" -import * as Numeric from "./Numeric.js" -export class NativeScriptError extends Data.TaggedError("NativeScriptError")<{ +/** + * Error class for Native script related operations. + * + * @since 2.0.0 + * @category errors + */ +export class NativeError extends Data.TaggedError("NativeError")<{ message?: string cause?: unknown }> {} -// CDDL specs for native scripts -// native_script = -// [ script_pubkey -// // script_all -// // script_any -// // script_n_of_k -// // invalid_before -// // invalid_hereafter -// ] - -// script_pubkey = (0, addr_keyhash) -// addr_keyhash = hash28 -// 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 - /** - * Schema for slot numbers used in time-based native script constraints. + * Type representing a native script following cardano-cli JSON syntax. * * @since 2.0.0 - * @category schemas + * @category model */ -export const SlotNumber = Numeric.Uint8Schema - -export type SlotNumber = typeof SlotNumber.Type - -// /** -// * @since 2.0.0 -// * @category model -// */ -export type NativeScript = ScriptPubKey | ScriptAll | ScriptAny | ScriptNOfK | InvalidBefore | InvalidHereafter - -export type NativeScriptEncoded = - | ScriptPubKeyEncoded - | ScriptAllEncoded - | ScriptAnyEncoded - | ScriptNOfKEncoded - | InvalidBeforeEncoded - | InvalidHereafterEncoded - -export interface ScriptPubKeyEncoded { - readonly _tag: "ScriptPubKey" - readonly keyHash: typeof KeyHash.KeyHash.Encoded -} -export interface ScriptAllEncoded { - readonly _tag: "ScriptAll" - readonly scripts: ReadonlyArray -} -export interface ScriptAnyEncoded { - readonly _tag: "ScriptAny" - readonly scripts: ReadonlyArray -} -export interface ScriptNOfKEncoded { - readonly _tag: "ScriptNOfK" - readonly n: bigint - readonly scripts: ReadonlyArray -} - -export interface InvalidBeforeEncoded { - readonly _tag: "InvalidBefore" - readonly slot: number -} - -export interface InvalidHereafterEncoded { - readonly _tag: "InvalidHereafter" - readonly slot: number -} - -export class ScriptPubKey extends Schema.TaggedClass("ScriptPubKey")("ScriptPubKey", { - keyHash: KeyHash.KeyHash -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: this._tag, - keyHash: this.keyHash +export type Native = + | { + type: "sig" + keyHash: string } - } -} - -export class ScriptAll extends Schema.TaggedClass("ScriptAll")("ScriptAll", { - scripts: Schema.Array(Schema.suspend((): Schema.Schema => NativeScript)) -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: this._tag, - scripts: this.scripts.map((script) => script) + | { + type: "before" + slot: number } - } -} - -export class ScriptAny extends Schema.TaggedClass("ScriptAny")("ScriptAny", { - scripts: Schema.Array(Schema.suspend((): Schema.Schema => NativeScript)) -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: this._tag, - scripts: this.scripts.map((script) => script) + | { + type: "after" + slot: number } - } -} - -export class ScriptNOfK extends Schema.TaggedClass("ScriptNOfK")("ScriptNOfK", { - n: Numeric.Int64, - scripts: Schema.Array(Schema.suspend((): Schema.Schema => NativeScript)) -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: this._tag, - n: this.n, - scripts: this.scripts.map((script) => script) + | { + type: "all" + scripts: ReadonlyArray } - } -} - -export class InvalidBefore extends Schema.TaggedClass("InvalidBefore")("InvalidBefore", { - slot: SlotNumber -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: this._tag, - slot: this.slot + | { + type: "any" + scripts: ReadonlyArray } - } -} - -export class InvalidHereafter extends Schema.TaggedClass("InvalidHereafter")("InvalidHereafter", { - slot: SlotNumber -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: this._tag, - slot: this.slot + | { + type: "atLeast" + required: number + scripts: ReadonlyArray } - } -} -export const NativeScript = Schema.Union( - ScriptPubKey, - ScriptAll, - ScriptAny, - ScriptNOfK, - InvalidBefore, - InvalidHereafter -) +/** + * 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. @@ -178,27 +116,27 @@ export const NativeScript = Schema.Union( * @category schemas */ -const ScriptPubKeyCDDL = Schema.Tuple(Schema.Literal(0), Schema.Uint8ArrayFromSelf) +const ScriptPubKeyCDDL = Schema.Tuple(Schema.Literal(0), Schema.String) const ScriptAllCDDL = Schema.Tuple( Schema.Literal(1), - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeScriptCDDL))) + Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeCDDL))) ) const ScriptAnyCDDL = Schema.Tuple( Schema.Literal(2), - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeScriptCDDL))) + Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeCDDL))) ) const ScriptNOfKCDDL = Schema.Tuple( Schema.Literal(3), - Schema.BigIntFromSelf, - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeScriptCDDL))) + CBOR.Integer, + Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeCDDL))) ) -const InvalidBeforeCDDL = Schema.Tuple(Schema.Literal(4), Schema.BigIntFromSelf) +const InvalidBeforeCDDL = Schema.Tuple(Schema.Literal(4), CBOR.Integer) -const InvalidHereafterCDDL = Schema.Tuple(Schema.Literal(5), Schema.BigIntFromSelf) +const InvalidHereafterCDDL = Schema.Tuple(Schema.Literal(5), CBOR.Integer) /** * CDDL representation of a native script as a union of tuple types. @@ -209,261 +147,296 @@ const InvalidHereafterCDDL = Schema.Tuple(Schema.Literal(5), Schema.BigIntFromSe * @since 2.0.0 * @category model */ -export type NativeScriptCDDL = - | readonly [0, Uint8Array] - | readonly [1, ReadonlyArray] - | readonly [2, ReadonlyArray] - | readonly [3, bigint, ReadonlyArray] +export type NativeCDDL = + | readonly [0, string] + | readonly [1, ReadonlyArray] + | readonly [2, ReadonlyArray] + | readonly [3, bigint, ReadonlyArray] | readonly [4, bigint] | readonly [5, bigint] /** - * Schema for NativeScriptCDDL union type. + * Schema for NativeCDDL union type. * * @since 2.0.0 * @category schemas */ -export const NativeScriptCDDL = Schema.transformOrFail( +export const NativeCDDL = Schema.transformOrFail( Schema.Union(ScriptPubKeyCDDL, ScriptAllCDDL, ScriptAnyCDDL, ScriptNOfKCDDL, InvalidBeforeCDDL, InvalidHereafterCDDL), - Schema.typeSchema(NativeScript), + Schema.typeSchema(Native), { strict: true, - encode: (nativeScript) => internalEncodeCDDL(nativeScript), + encode: (native) => internalEncodeCDDL(native), decode: (cborTuple) => internalDecodeCDDL(cborTuple) } ) /** - * Convert a NativeScript to its CDDL representation. + * Convert a Native to its CDDL representation. * * @since 2.0.0 * @category encoding */ -export const internalEncodeCDDL = (nativeScript: NativeScript): Effect.Effect => - Effect.gen(function* () { - switch (nativeScript._tag) { - case "ScriptPubKey": { - return [0, yield* ParseResult.encode(KeyHash.FromBytes)(nativeScript.keyHash)] +export const internalEncodeCDDL = (native: Native): Eff.Effect => + Eff.gen(function* () { + switch (native.type) { + case "sig": { + return [0, native.keyHash] as const } - case "ScriptAll": { - const scripts = yield* Effect.forEach(nativeScript.scripts, internalEncodeCDDL) + case "all": { + const scripts = yield* Eff.forEach(native.scripts, internalEncodeCDDL) return [1, scripts] as const } - case "ScriptAny": { - const scripts = yield* Effect.forEach(nativeScript.scripts, internalEncodeCDDL) + case "any": { + const scripts = yield* Eff.forEach(native.scripts, internalEncodeCDDL) return [2, scripts] as const } - case "ScriptNOfK": { - const scripts = yield* Effect.forEach(nativeScript.scripts, internalEncodeCDDL) - return [3, nativeScript.n, scripts] as const + case "atLeast": { + const scripts = yield* Eff.forEach(native.scripts, internalEncodeCDDL) + return [3, BigInt(native.required), scripts] as const } - case "InvalidBefore": { - return [4, BigInt(nativeScript.slot)] as const + case "before": { + return [4, BigInt(native.slot)] as const } - case "InvalidHereafter": { - return [5, BigInt(nativeScript.slot)] as const + case "after": { + return [5, BigInt(native.slot)] as const } } }) /** - * Convert a CDDL representation back to a NativeScript. + * Convert a CDDL representation back to a Native. * * This function recursively decodes nested CBOR scripts and constructs - * the appropriate NativeScript instances. + * the appropriate Native instances. * * @since 2.0.0 * @category decoding */ -export const internalDecodeCDDL = (cborTuple: NativeScriptCDDL): Effect.Effect => - Effect.gen(function* () { +export const internalDecodeCDDL = (cborTuple: NativeCDDL): Eff.Effect => + Eff.gen(function* () { switch (cborTuple[0]) { case 0: { - // ScriptPubKey: [0, keyHash_bytes] - const [, keyHashBytes] = cborTuple - const keyHash = yield* ParseResult.decode(KeyHash.FromBytes)(keyHashBytes) - return new ScriptPubKey({ keyHash }) + // sig: [0, keyHash_string] + const [, keyHash] = cborTuple + return { + type: "sig" as const, + keyHash + } } case 1: { - // ScriptAll: [1, [native_script, ...]] + // all: [1, [native_script, ...]] const [, scriptCBORs] = cborTuple - const scripts: Array = [] + const scripts: Array = [] for (const scriptCBOR of scriptCBORs) { const script = yield* internalDecodeCDDL(scriptCBOR) scripts.push(script) } - return new ScriptAll({ scripts }) + return { + type: "all" as const, + scripts + } } case 2: { - // ScriptAny: [2, [native_script, ...]] + // any: [2, [native_script, ...]] const [, scriptCBORs] = cborTuple - const scripts: Array = [] + const scripts: Array = [] for (const scriptCBOR of scriptCBORs) { const script = yield* internalDecodeCDDL(scriptCBOR) scripts.push(script) } - return new ScriptAny({ scripts }) + return { + type: "any" as const, + scripts + } } case 3: { - // ScriptNOfK: [3, n, [native_script, ...]] - const [, n, scriptCBORs] = cborTuple - const scripts: Array = [] + // 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 new ScriptNOfK({ - n: Numeric.Int64.make(n), + return { + type: "atLeast" as const, + required: Number(required), scripts - }) + } } case 4: { - // InvalidBefore: [4, slot] + // before: [4, slot] const [, slot] = cborTuple - return new InvalidBefore({ + return { + type: "before" as const, slot: Number(slot) - }) + } } case 5: { - // InvalidHereafter: [5, slot] + // after: [5, slot] const [, slot] = cborTuple - return new InvalidHereafter({ + return { + type: "after" as const, slot: Number(slot) - }) + } } default: // This should never happen with proper CBOR validation - throw new Error(`Invalid native script tag: ${cborTuple[0]}`) + return yield* Eff.fail(new ParseResult.Type(Schema.Literal(0, 1, 2, 3, 4, 5).ast, cborTuple[0])) } }) -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +/** + * 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 - NativeScriptCDDL // CBOR → NativeScript - ) + NativeCDDL // CBOR → Native + ).annotations({ + identifier: "Native.FromCBORBytes", + title: "Native from CBOR Bytes", + description: "Transforms CBOR bytes to Native" + }) -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +/** + * 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( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → NativeScript - ) + FromCBORBytes(options) // Uint8Array → Native + ).annotations({ + identifier: "Native.FromCBORHex", + title: "Native from CBOR Hex", + description: "Transforms CBOR hex string to Native" + }) + +/** + * Root Functions + * ============================================================================ + */ /** - * Schema transformer for converting between NativeJSON and NativeScript. + * Parse Native from CBOR bytes. * * @since 2.0.0 - * @category schemas + * @category parsing */ -export const NativeJSON = Schema.transformOrFail(NativeScriptJSON.NativeJSONSchema, NativeScript, { - strict: true, - encode: (_, __, ___, toI) => internalNativeToJson(toI), - decode: (fromA) => internalJSONToNative(fromA) -}) +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Native => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) /** - * Convert a NativeJSON to a NativeScript. + * Parse Native from CBOR hex string. * * @since 2.0.0 - * @category conversion + * @category parsing */ -export const internalJSONToNative = ( - nativeJSON: NativeScriptJSON.NativeJSON -): Effect.Effect => - Effect.gen(function* () { - switch (nativeJSON.type) { - case "sig": { - const keyHash = yield* ParseResult.decode(KeyHash.FromHex)(nativeJSON.keyHash) - return new ScriptPubKey({ keyHash }) - } - case "before": { - return new InvalidBefore({ slot: nativeJSON.slot }) - } - case "after": { - return new InvalidHereafter({ slot: nativeJSON.slot }) - } - case "all": { - const scripts = yield* Effect.forEach(nativeJSON.scripts, internalJSONToNative) - return new ScriptAll({ scripts }) - } - case "any": { - const scripts = yield* Effect.forEach(nativeJSON.scripts, internalJSONToNative) - return new ScriptAny({ scripts }) - } - case "atLeast": { - const scripts = yield* Effect.forEach(nativeJSON.scripts, internalJSONToNative) - return new ScriptNOfK({ - n: Numeric.Int64.make(BigInt(nativeJSON.required)), - scripts - }) - } - } - }) +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Native => + Eff.runSync(Effect.fromCBORHex(hex, options)) /** - * Convert a NativeScript to a NativeJSON. + * Encode Native to CBOR bytes. * * @since 2.0.0 - * @category conversion + * @category encoding */ -export const internalNativeToJson = ( - nativeScript: NativeScript -): Effect.Effect => - Effect.gen(function* () { - switch (nativeScript._tag) { - case "ScriptPubKey": { - return { - type: "sig" as const, - keyHash: yield* ParseResult.encode(KeyHash.FromHex)(nativeScript.keyHash) - } - } - case "ScriptAll": { - const scripts = yield* Effect.forEach(nativeScript.scripts, internalNativeToJson) - return { - type: "all" as const, - scripts - } - } - case "ScriptAny": { - const scripts = yield* Effect.forEach(nativeScript.scripts, internalNativeToJson) - return { - type: "any" as const, - scripts - } - } - case "ScriptNOfK": { - const scripts = yield* Effect.forEach(nativeScript.scripts, internalNativeToJson) - return { - type: "atLeast" as const, - required: Number(nativeScript.n), - scripts - } - } - case "InvalidBefore": { - return { - type: "before" as const, - slot: nativeScript.slot - } - } - case "InvalidHereafter": { - return { - type: "after" as const, - slot: nativeScript.slot - } - } - default: - throw new Error( - `Exhaustive check failed: Unhandled case '${(nativeScript as unknown as { _tag: string })._tag}' encountered.` - ) - } - }) +export const toCBORBytes = (native: Native, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(native, options)) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options), - nativeJSON: NativeJSON - }, - NativeScriptError - ) +/** + * Encode Native to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (native: Native, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(native, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Native from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new NativeError({ + message: "Failed to parse Native from CBOR bytes", + cause + }) + ) + ) + + /** + * Parse Native from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new NativeError({ + message: "Failed to parse Native from CBOR hex", + cause + }) + ) + ) + + /** + * Encode Native to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (native: Native, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORBytes(options))(native).pipe( + Eff.mapError( + (cause) => + new NativeError({ + message: "Failed to encode Native to CBOR bytes", + cause + }) + ) + ) + + /** + * Encode Native to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (native: Native, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORHex(options))(native).pipe( + Eff.mapError( + (cause) => + new NativeError({ + message: "Failed to encode Native to CBOR hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/Natural.ts b/packages/evolution/src/Natural.ts index a1961110..b581b9bb 100644 --- a/packages/evolution/src/Natural.ts +++ b/packages/evolution/src/Natural.ts @@ -1,23 +1,150 @@ -import { FastCheck, Schema } from "effect" +import { Data, Either as E, FastCheck, Schema } from "effect" /** - * Natural number constructors - * Used for validating non negative integers + * Error class for Natural related operations. * * @since 2.0.0 + * @category errors */ -export const Natural = Schema.Positive.pipe(Schema.brand("Natural")) +export class NaturalError extends Data.TaggedError("NaturalError")<{ + message?: string + cause?: unknown +}> {} +/** + * Natural number schema for positive integers. + * Used for validating non-negative integers greater than 0. + * + * @since 2.0.0 + * @category schemas + */ +export const Natural = Schema.Positive.pipe(Schema.brand("Natural")).annotations({ + identifier: "Natural", + title: "Natural Number", + description: "A positive integer greater than 0" +}) + +/** + * Type alias for Natural representing positive integers. + * + * @since 2.0.0 + * @category model + */ export type Natural = typeof Natural.Type /** - * Check if the given value is a valid PositiveNumber + * Smart constructor for Natural that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = Natural.make + +/** + * Check if two Natural instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: Natural, b: Natural): boolean => a === b + +/** + * Check if the given value is a valid Natural * * @since 2.0.0 * @category predicates */ +export const isNatural = Schema.is(Natural) -export const generator = FastCheck.integer({ +/** + * FastCheck arbitrary for generating random Natural instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.integer({ min: 1, max: Number.MAX_SAFE_INTEGER -}).map((number) => Natural.make(number)) +}).map((number) => number as Natural) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse Natural from number. + * + * @since 2.0.0 + * @category parsing + */ +export const fromNumber = (num: number): Natural => { + try { + return Schema.decodeSync(Natural)(num) + } catch (cause) { + throw new NaturalError({ + message: "Failed to parse Natural from number", + cause + }) + } +} + +/** + * Encode Natural to number. + * + * @since 2.0.0 + * @category encoding + */ +export const toNumber = (natural: Natural): number => { + try { + return Schema.encodeSync(Natural)(natural) + } catch (cause) { + throw new NaturalError({ + message: "Failed to encode Natural to number", + cause + }) + } +} + +// ============================================================================ +// Either Namespace +// ============================================================================ + +/** + * Either-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category either + */ +export namespace Either { + /** + * Parse Natural from number with Either error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromNumber = (num: number): E.Either => + E.mapLeft( + Schema.decodeEither(Natural)(num), + (cause) => + new NaturalError({ + message: "Failed to parse Natural from number", + cause + }) + ) + + /** + * Encode Natural to number with Either error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toNumber = (natural: Natural): E.Either => + E.mapLeft( + Schema.encodeEither(Natural)(natural), + (cause) => + new NaturalError({ + message: "Failed to encode Natural to number", + cause + }) + ) +} diff --git a/packages/evolution/src/Network.ts b/packages/evolution/src/Network.ts index ab2eb161..ca89560e 100644 --- a/packages/evolution/src/Network.ts +++ b/packages/evolution/src/Network.ts @@ -1,35 +1,180 @@ -import { Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as NetworkId from "./NetworkId.js" -const Network = Schema.Literal("Mainnet", "Preview", "Preprod", "Custom") -type Network = typeof Network.Type +/** + * Error class for Network related operations. + * + * @since 2.0.0 + * @category errors + */ +export class NetworkError extends Data.TaggedError("NetworkError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Schema for Network representing Cardano network types. + * Supports Mainnet, Preview, Preprod, and Custom networks. + * + * @since 2.0.0 + * @category schemas + */ +export const Network = Schema.String.pipe( + Schema.filter((str): str is "Mainnet" | "Preview" | "Preprod" | "Custom" => + str === "Mainnet" || str === "Preview" || str === "Preprod" || str === "Custom" + ), + Schema.brand("Network") +).annotations({ + identifier: "Network", + title: "Cardano Network", + description: "A Cardano network type (Mainnet, Preview, Preprod, or Custom)" +}) + +/** + * Type alias for Network representing Cardano network types. + * + * @since 2.0.0 + * @category model + */ +export type Network = typeof Network.Type + +/** + * Smart constructor for Network that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = Schema.decodeSync(Network) + +/** + * Check if two Network instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: Network, b: Network): boolean => a === b + +/** + * Check if a value is a valid Network. + * + * @since 2.0.0 + * @category predicates + */ +export const is = (value: unknown): value is Network => Schema.is(Network)(value) /** - * Converts a Network type to Id number + * FastCheck arbitrary for generating random Network instances. * - * @since 1.0.0 + * @since 2.0.0 + * @category arbitrary */ -const _toId = (network: T): NetworkId.NetworkId => { +export const arbitrary = FastCheck.constantFrom("Mainnet", "Preview", "Preprod", "Custom").map((literal) => + make(literal) +) + +/** + * Converts a Network type to NetworkId number. + * + * @since 2.0.0 + * @category conversion + */ +export const toId = (network: T): NetworkId.NetworkId => { switch (network) { case "Preview": case "Preprod": case "Custom": - return NetworkId.NetworkId.make(0) + return NetworkId.make(0) case "Mainnet": - return NetworkId.NetworkId.make(1) + return NetworkId.make(1) default: throw new Error(`Exhaustive check failed: Unhandled case ${network}`) } } -const _fromId = (id: NetworkId.NetworkId): Network => { +/** + * Converts a NetworkId to Network type. + * + * @since 2.0.0 + * @category conversion + */ +export const fromId = (id: NetworkId.NetworkId): Network => { switch (id) { case 0: - return "Preview" + return make("Preview") case 1: - return "Mainnet" + return make("Mainnet") default: throw new Error(`Exhaustive check failed: Unhandled case ${id}`) } } + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse Network from string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromString = (str: string): Network => + Eff.runSync(Effect.fromString(str)) + +/** + * Encode Network to string. + * + * @since 2.0.0 + * @category encoding + */ +export const toString = (network: Network): string => + Eff.runSync(Effect.toString(network)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Network from string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromString = (str: string): Eff.Effect => + Schema.decode(Network)(str).pipe( + Eff.mapError( + (cause) => + new NetworkError({ + message: "Failed to parse Network from string", + cause + }) + ) + ) + + /** + * Encode Network to string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toString = (network: Network): Eff.Effect => + Schema.encode(Network)(network).pipe( + Eff.mapError( + (cause) => + new NetworkError({ + message: "Failed to encode Network to string", + cause + }) + ) + ) +} + +// ============================================================================ diff --git a/packages/evolution/src/NetworkId.ts b/packages/evolution/src/NetworkId.ts index fbe92cea..fbf27881 100644 --- a/packages/evolution/src/NetworkId.ts +++ b/packages/evolution/src/NetworkId.ts @@ -1,12 +1,53 @@ -import { FastCheck, Schema } from "effect" +import { Data, FastCheck, Schema } from "effect" -export const NetworkId = Schema.NonNegativeInt.pipe(Schema.brand("NetworkId")) +/** + * Error class for NetworkId related operations. + * + * @since 2.0.0 + * @category errors + */ +export class NetworkIdError extends Data.TaggedError("NetworkIdError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Schema for NetworkId representing a Cardano network identifier. + * 0 = Testnet, 1 = Mainnet + * + * @since 2.0.0 + * @category schemas + */ +export const NetworkId = Schema.NonNegativeInt.pipe(Schema.brand("NetworkId")).annotations({ + identifier: "NetworkId" +}) export type NetworkId = typeof NetworkId.Type +/** + * Smart constructor for NetworkId that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ export const make = NetworkId.make -export const generator = FastCheck.integer({ +/** + * Check if two NetworkId instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: NetworkId, b: NetworkId): boolean => a === b + +/** + * FastCheck generator for creating NetworkId instances. + * Generates values 0 (Testnet) or 1 (Mainnet). + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.integer({ min: 0, max: 2 }).map((number) => make(number)) diff --git a/packages/evolution/src/NonZeroInt64.ts b/packages/evolution/src/NonZeroInt64.ts index 7a0cbc6f..25e4d8de 100644 --- a/packages/evolution/src/NonZeroInt64.ts +++ b/packages/evolution/src/NonZeroInt64.ts @@ -1,4 +1,4 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Either as E, FastCheck, Schema } from "effect" /** * Constants for NonZeroInt64 validation. @@ -31,11 +31,10 @@ export class NonZeroInt64Error extends Data.TaggedError("NonZeroInt64Error")<{ * @since 2.0.0 * @category schemas */ -export const NegInt64Schema = pipe( - Schema.BigIntFromSelf, - Schema.filter((value) => value >= NEG_INT64_MIN && value <= NEG_INT64_MAX) +export const NegInt64Schema = Schema.BigIntFromSelf.pipe( + Schema.filter((value: bigint) => value >= NEG_INT64_MIN && value <= NEG_INT64_MAX) ).annotations({ - message: (issue) => `NegInt64 must be between ${NEG_INT64_MIN} and ${NEG_INT64_MAX}, but got ${issue.actual}`, + message: (issue: any) => `NegInt64 must be between ${NEG_INT64_MIN} and ${NEG_INT64_MAX}, but got ${issue.actual}`, identifier: "NegInt64" }) @@ -45,11 +44,10 @@ export const NegInt64Schema = pipe( * @since 2.0.0 * @category schemas */ -export const PosInt64Schema = pipe( - Schema.BigIntFromSelf, - Schema.filter((value) => value >= POS_INT64_MIN && value <= POS_INT64_MAX) +export const PosInt64Schema = Schema.BigIntFromSelf.pipe( + Schema.filter((value: bigint) => value >= POS_INT64_MIN && value <= POS_INT64_MAX) ).annotations({ - message: (issue) => `PosInt64 must be between ${POS_INT64_MIN} and ${POS_INT64_MAX}, but got ${issue.actual}`, + message: (issue: any) => `PosInt64 must be between ${POS_INT64_MIN} and ${POS_INT64_MAX}, but got ${issue.actual}`, identifier: "PosInt64" }) @@ -60,9 +58,13 @@ export const PosInt64Schema = pipe( * @since 2.0.0 * @category schemas */ -export const NonZeroInt64Schema = Schema.Union(NegInt64Schema, PosInt64Schema).annotations({ - identifier: "NonZeroInt64" -}) +export const NonZeroInt64 = Schema.Union(NegInt64Schema, PosInt64Schema) + .pipe(Schema.brand("NonZeroInt64")) + .annotations({ + identifier: "NonZeroInt64", + title: "Non-Zero 64-bit Integer", + description: "A non-zero signed 64-bit integer (-9223372036854775808 to -1 or 1 to 9223372036854775807)" + }) /** * Type alias for NonZeroInt64 representing non-zero signed 64-bit integers. @@ -71,7 +73,7 @@ export const NonZeroInt64Schema = Schema.Union(NegInt64Schema, PosInt64Schema).a * @since 2.0.0 * @category model */ -export type NonZeroInt64 = typeof NonZeroInt64Schema.Type +export type NonZeroInt64 = typeof NonZeroInt64.Type /** * Smart constructor for creating NonZeroInt64 values. @@ -79,7 +81,7 @@ export type NonZeroInt64 = typeof NonZeroInt64Schema.Type * @since 2.0.0 * @category constructors */ -export const make = Schema.decodeSync(NonZeroInt64Schema) +export const make = Schema.decodeSync(NonZeroInt64) /** * Check if a value is a valid NonZeroInt64. @@ -87,7 +89,7 @@ export const make = Schema.decodeSync(NonZeroInt64Schema) * @since 2.0.0 * @category predicates */ -export const is = Schema.is(NonZeroInt64Schema) +export const is = Schema.is(NonZeroInt64) /** * Check if a NonZeroInt64 is positive. @@ -111,7 +113,16 @@ export const isNegative = (value: NonZeroInt64): boolean => value < 0n * @since 2.0.0 * @category transformation */ -export const abs = (value: NonZeroInt64): NonZeroInt64 => (value < 0n ? make(-value) : value) +export const abs = (value: NonZeroInt64): NonZeroInt64 => { + try { + return Schema.decodeSync(NonZeroInt64)(value < 0n ? -value : value) + } catch (cause) { + throw new NonZeroInt64Error({ + message: "Failed to get absolute value of NonZeroInt64", + cause + }) + } +} /** * Negate a NonZeroInt64. @@ -119,7 +130,16 @@ export const abs = (value: NonZeroInt64): NonZeroInt64 => (value < 0n ? make(-va * @since 2.0.0 * @category transformation */ -export const negate = (value: NonZeroInt64): NonZeroInt64 => make(-value) +export const negate = (value: NonZeroInt64): NonZeroInt64 => { + try { + return Schema.decodeSync(NonZeroInt64)(-value) + } catch (cause) { + throw new NonZeroInt64Error({ + message: "Failed to negate NonZeroInt64", + cause + }) + } +} /** * Compare two NonZeroInt64 values. @@ -142,12 +162,94 @@ export const compare = (a: NonZeroInt64, b: NonZeroInt64): -1 | 0 | 1 => { export const equals = (a: NonZeroInt64, b: NonZeroInt64): boolean => a === b /** - * Generate a random NonZeroInt64. + * FastCheck arbitrary for generating random NonZeroInt64 instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.oneof( +export const arbitrary = FastCheck.oneof( FastCheck.bigInt({ min: NEG_INT64_MIN, max: NEG_INT64_MAX }), FastCheck.bigInt({ min: POS_INT64_MIN, max: POS_INT64_MAX }) ) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse NonZeroInt64 from bigint. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBigInt = (value: bigint): NonZeroInt64 => { + try { + return Schema.decodeSync(NonZeroInt64)(value) + } catch (cause) { + throw new NonZeroInt64Error({ + message: "Failed to parse NonZeroInt64 from bigint", + cause + }) + } +} + +/** + * Encode NonZeroInt64 to bigint. + * + * @since 2.0.0 + * @category encoding + */ +export const toBigInt = (value: NonZeroInt64): bigint => { + try { + return Schema.encodeSync(NonZeroInt64)(value) + } catch (cause) { + throw new NonZeroInt64Error({ + message: "Failed to encode NonZeroInt64 to bigint", + cause + }) + } +} + +// ============================================================================ +// Either Namespace +// ============================================================================ + +/** + * Either-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category either + */ +export namespace Either { + /** + * Parse NonZeroInt64 from bigint with Either error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBigInt = (value: bigint): E.Either => + E.mapLeft( + Schema.decodeEither(NonZeroInt64)(value), + (cause) => + new NonZeroInt64Error({ + message: "Failed to parse NonZeroInt64 from bigint", + cause + }) + ) + + /** + * Encode NonZeroInt64 to bigint with Either error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBigInt = (value: NonZeroInt64): E.Either => + E.mapLeft( + Schema.encodeEither(NonZeroInt64)(value), + (cause) => + new NonZeroInt64Error({ + message: "Failed to encode NonZeroInt64 to bigint", + cause + }) + ) +} diff --git a/packages/evolution/src/Numeric.ts b/packages/evolution/src/Numeric.ts index 2140e34b..782a156d 100644 --- a/packages/evolution/src/Numeric.ts +++ b/packages/evolution/src/Numeric.ts @@ -1,22 +1,60 @@ -import { FastCheck, Schema } from "effect" +import { Data, FastCheck, Schema } from "effect" + +/** + * Error class for Numeric related operations. + * + * @since 2.0.0 + * @category errors + */ +export class NumericError extends Data.TaggedError("NumericError")<{ + message?: string + cause?: unknown +}> {} export const UINT8_MIN = 0 export const UINT8_MAX = 255 +/** + * Schema for 8-bit unsigned integers. + * + * @since 2.0.0 + * @category schemas + */ export const Uint8Schema = Schema.Number.pipe( Schema.filter((number) => Number.isInteger(number) && number >= UINT8_MIN && number <= UINT8_MAX), Schema.annotations({ identifier: "Uint8", + title: "8-bit Unsigned Integer", description: `An 8-bit unsigned integer (${UINT8_MIN} to ${UINT8_MAX})` }) ) +/** + * Type alias for 8-bit unsigned integers. + * + * @since 2.0.0 + * @category model + */ export type Uint8 = typeof Uint8Schema.Type +/** + * Smart constructor for Uint8 that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const Uint8Make = Uint8Schema.make + +/** + * FastCheck arbitrary for generating random Uint8 instances. + * + * @since 2.0.0 + * @category arbitrary + */ export const Uint8Generator = FastCheck.integer({ min: UINT8_MIN, max: UINT8_MAX -}).map((number) => Uint8Schema.make(number)) +}).map(Uint8Make) export const UINT16_MIN = 0 export const UINT16_MAX = 65535 @@ -25,16 +63,25 @@ export const Uint16Schema = Schema.Number.pipe( Schema.filter((number) => Number.isInteger(number) && number >= UINT16_MIN && number <= UINT16_MAX), Schema.annotations({ identifier: "Uint16", + title: "16-bit Unsigned Integer", description: `A 16-bit unsigned integer (${UINT16_MIN} to ${UINT16_MAX})` }) ) export type Uint16 = typeof Uint16Schema.Type +/** + * Smart constructor for Uint16 that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const Uint16Make = Uint16Schema.make + export const Uint16Generator = FastCheck.integer({ min: UINT16_MIN, max: UINT16_MAX -}).map((number) => Uint16Schema.make(number)) +}).map(Uint16Make) export const UINT32_MIN = 0 export const UINT32_MAX = 4294967295 @@ -43,16 +90,25 @@ export const Uint32Schema = Schema.Number.pipe( Schema.filter((number) => Number.isInteger(number) && number >= UINT32_MIN && number <= UINT32_MAX), Schema.annotations({ identifier: "Uint32", + title: "32-bit Unsigned Integer", description: `A 32-bit unsigned integer (${UINT32_MIN} to ${UINT32_MAX})` }) ) export type Uint32 = typeof Uint32Schema.Type +/** + * Smart constructor for Uint32 that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const Uint32Make = Uint32Schema.make + export const Uint32Generator = FastCheck.integer({ min: UINT32_MIN, max: UINT32_MAX -}).map((number) => Uint32Schema.make(number)) +}).map(Uint32Make) export const UINT64_MIN = 0n export const UINT64_MAX = 18446744073709551615n @@ -60,14 +116,24 @@ export const Uint64Schema = Schema.BigIntFromSelf.pipe( Schema.filter((bigint) => bigint >= UINT64_MIN && bigint <= UINT64_MAX), Schema.annotations({ identifier: "Uint64", + title: "64-bit Unsigned Integer", description: `A 64-bit unsigned integer (${UINT64_MIN} to ${UINT64_MAX})` }) ) export type Uint64 = typeof Uint64Schema.Type + +/** + * Smart constructor for Uint64 that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const Uint64Make = Uint64Schema.make + export const Uint64Generator = FastCheck.bigInt({ min: UINT64_MIN, max: UINT64_MAX -}).map((bigint) => Uint64Schema.make(bigint)) +}).map(Uint64Make) export const INT8_MIN = -128 export const INT8_MAX = 127 @@ -76,16 +142,25 @@ export const Int8 = Schema.Number.pipe( Schema.filter((number) => Number.isInteger(number) && number >= INT8_MIN && number <= INT8_MAX), Schema.annotations({ identifier: "Int8", + title: "8-bit Signed Integer", description: `An 8-bit signed integer (${INT8_MIN} to ${INT8_MAX})` }) ) export type Int8 = typeof Int8.Type +/** + * Smart constructor for Int8 that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const Int8Make = Int8.make + export const Int8Generator = FastCheck.integer({ min: INT8_MIN, max: INT8_MAX -}).map((number) => Int8.make(number)) +}).map(Int8Make) export const INT16_MIN = -32768 export const INT16_MAX = 32767 @@ -94,16 +169,25 @@ export const Int16 = Schema.Number.pipe( Schema.filter((number) => Number.isInteger(number) && number >= INT16_MIN && number <= INT16_MAX), Schema.annotations({ identifier: "Int16", + title: "16-bit Signed Integer", description: `A 16-bit signed integer (${INT16_MIN} to ${INT16_MAX})` }) ) export type Int16 = typeof Int16.Type +/** + * Smart constructor for Int16 that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const Int16Make = Int16.make + export const Int16Generator = FastCheck.integer({ min: INT16_MIN, max: INT16_MAX -}).map((number) => Int16.make(number)) +}).map(Int16Make) export const INT32_MIN = -2147483648 export const INT32_MAX = 2147483647 @@ -112,16 +196,25 @@ export const Int32 = Schema.Number.pipe( Schema.filter((number) => Number.isInteger(number) && number >= INT32_MIN && number <= INT32_MAX), Schema.annotations({ identifier: "Int32", + title: "32-bit Signed Integer", description: `A 32-bit signed integer (${INT32_MIN} to ${INT32_MAX})` }) ) export type Int32 = typeof Int32.Type +/** + * Smart constructor for Int32 that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const Int32Make = Int32.make + export const Int32Generator = FastCheck.integer({ min: INT32_MIN, max: INT32_MAX -}).map((number) => Int32.make(number)) +}).map(Int32Make) export const INT64_MIN = -9223372036854775808n export const INT64_MAX = 9223372036854775807n @@ -130,13 +223,22 @@ export const Int64 = Schema.BigIntFromSelf.pipe( Schema.filter((bigint) => bigint >= INT64_MIN && bigint <= INT64_MAX), Schema.annotations({ identifier: "Int64", + title: "64-bit Signed Integer", description: `A 64-bit signed integer (${INT64_MIN} to ${INT64_MAX})` }) ) export type Int64 = typeof Int64.Type +/** + * Smart constructor for Int64 that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const Int64Make = Int64.make + export const Int64Generator = FastCheck.bigInt({ min: INT64_MIN, max: INT64_MAX -}).map((bigint) => Int64.make(bigint)) +}).map(Int64Make) diff --git a/packages/evolution/src/OperationalCert.ts b/packages/evolution/src/OperationalCert.ts index 1e0437e5..118e09a6 100644 --- a/packages/evolution/src/OperationalCert.ts +++ b/packages/evolution/src/OperationalCert.ts @@ -1,8 +1,7 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as Ed25519Signature from "./Ed25519Signature.js" import * as KESVkey from "./KESVkey.js" import * as Numeric from "./Numeric.js" @@ -38,18 +37,6 @@ export class OperationalCert extends Schema.TaggedClass()("Oper sigma: Ed25519Signature.Ed25519Signature }) {} -/** - * Check if two OperationalCert instances are equal. - * - * @since 2.0.0 - * @category equality - */ -export const equals = (a: OperationalCert, b: OperationalCert): boolean => - KESVkey.equals(a.hotVkey, b.hotVkey) && - a.sequenceNumber === b.sequenceNumber && - a.kesPeriod === b.kesPeriod && - Ed25519Signature.equals(a.sigma, b.sigma) - /** * CDDL schema for OperationalCert. * operational_cert = [ @@ -73,13 +60,13 @@ export const FromCDDL = Schema.transformOrFail( { strict: true, encode: (toA) => - Effect.gen(function* () { + Eff.gen(function* () { const hotVkeyBytes = yield* ParseResult.encode(KESVkey.FromBytes)(toA.hotVkey) const sigmaBytes = yield* ParseResult.encode(Ed25519Signature.FromBytes)(toA.sigma) return [hotVkeyBytes, BigInt(toA.sequenceNumber), BigInt(toA.kesPeriod), sigmaBytes] as const }), decode: ([hotVkeyBytes, sequenceNumber, kesPeriod, sigmaBytes]) => - Effect.gen(function* () { + Eff.gen(function* () { const hotVkey = yield* ParseResult.decode(KESVkey.FromBytes)(hotVkeyBytes) const sigma = yield* ParseResult.decode(Ed25519Signature.FromBytes)(sigmaBytes) return yield* ParseResult.decode(OperationalCert)({ @@ -99,7 +86,7 @@ export const FromCDDL = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → OperationalCert @@ -111,17 +98,161 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → OperationalCert + FromCBORBytes(options) // Uint8Array → OperationalCert ) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - OperationalCertError - ) +/** + * Check if two OperationalCert instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: OperationalCert, b: OperationalCert): boolean => + KESVkey.equals(a.hotVkey, b.hotVkey) && + a.sequenceNumber === b.sequenceNumber && + a.kesPeriod === b.kesPeriod && + Ed25519Signature.equals(a.sigma, b.sigma) + +/** + * Check if the given value is a valid OperationalCert + * + * @since 2.0.0 + * @category predicates + */ +export const isOperationalCert = Schema.is(OperationalCert) + +/** + * FastCheck arbitrary for generating random OperationalCert instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.record({ + hotVkey: KESVkey.arbitrary, + sequenceNumber: FastCheck.bigUint(), + kesPeriod: FastCheck.bigUint(), + sigma: Ed25519Signature.arbitrary +}).map((props) => new OperationalCert(props)) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse OperationalCert from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): OperationalCert => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse OperationalCert from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): OperationalCert => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Encode OperationalCert to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (cert: OperationalCert, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(cert, options)) + +/** + * Encode OperationalCert to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (cert: OperationalCert, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(cert, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse OperationalCert from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new OperationalCertError({ + message: "Failed to parse OperationalCert from CBOR bytes", + cause + }) + ) + ) + + /** + * Parse OperationalCert from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new OperationalCertError({ + message: "Failed to parse OperationalCert from CBOR hex", + cause + }) + ) + ) + + /** + * Encode OperationalCert to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (cert: OperationalCert, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORBytes(options))(cert).pipe( + Eff.mapError( + (cause) => + new OperationalCertError({ + message: "Failed to encode OperationalCert to CBOR bytes", + cause + }) + ) + ) + + /** + * Encode OperationalCert to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (cert: OperationalCert, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORHex(options))(cert).pipe( + Eff.mapError( + (cause) => + new OperationalCertError({ + message: "Failed to encode OperationalCert to CBOR hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/Pointer.ts b/packages/evolution/src/Pointer.ts index 4631c7cf..d2f0beaf 100644 --- a/packages/evolution/src/Pointer.ts +++ b/packages/evolution/src/Pointer.ts @@ -1,4 +1,4 @@ -import { Schema } from "effect" +import { Schema, FastCheck } from "effect" import * as Natural from "./Natural.js" @@ -49,3 +49,29 @@ export const make = (slot: Natural.Natural, txIndex: Natural.Natural, certIndex: { disableValidation: true } ) } + +/** + * Check if two Pointer instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: Pointer, b: Pointer): boolean => { + return ( + a.slot === b.slot && + a.txIndex === b.txIndex && + a.certIndex === b.certIndex + ) +} + +/** + * FastCheck arbitrary for generating random Pointer instances + * + * @since 2.0.0 + * @category generators + */ +export const arbitrary = FastCheck.tuple( + Natural.arbitrary, + Natural.arbitrary, + Natural.arbitrary +).map(([slot, txIndex, certIndex]) => make(slot, txIndex, certIndex)) diff --git a/packages/evolution/src/PointerAddress.ts b/packages/evolution/src/PointerAddress.ts index fc239c76..0a3835c1 100644 --- a/packages/evolution/src/PointerAddress.ts +++ b/packages/evolution/src/PointerAddress.ts @@ -1,7 +1,6 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" -import * as _Codec from "./Codec.js" import * as Credential from "./Credential.js" import * as KeyHash from "./KeyHash.js" import * as Natural from "./Natural.js" @@ -44,7 +43,7 @@ export class PointerAddress extends Schema.TaggedClass("PointerA export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, PointerAddress, { strict: true, encode: (toI, options, ast, toA) => - Effect.gen(function* () { + Eff.gen(function* () { const paymentBit = toA.paymentCredential._tag === "KeyHash" ? 0 : 1 const header = (0b01 << 6) | (0b0 << 5) | (paymentBit << 4) | (toA.networkId & 0b00001111) @@ -76,7 +75,7 @@ export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, Point return result }), decode: (_, __, ast, fromA) => - Effect.gen(function* () { + Eff.gen(function* () { const header = fromA[0] // Extract network ID from the lower 4 bits const networkId = header & 0b00001111 @@ -94,7 +93,7 @@ export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, Point : { _tag: "ScriptHash", - hash: yield* ParseResult.decode(ScriptHash.BytesSchema)(fromA.slice(1, 29)) + hash: yield* ParseResult.decode(ScriptHash.FromBytes)(fromA.slice(1, 29)) } // After the credential, we have 3 variable-length integers @@ -115,13 +114,19 @@ export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, Point paymentCredential, pointer: Pointer.make(slot, txIndex, certIndex) }) - }).pipe(Effect.catchTag("PointerAddressError", (e) => Effect.fail(new ParseResult.Type(ast, fromA, e.message)))) + }).pipe(Eff.catchTag("PointerAddressError", (e) => Eff.fail(new ParseResult.Type(ast, fromA, e.message)))) +}).annotations({ + identifier: "PointerAddress.FromBytes", + description: "Transforms raw bytes to PointerAddress" }) export const FromHex = Schema.compose( Bytes.FromHex, // string → Uint8Array FromBytes // Uint8Array → PointerAddress -) +).annotations({ + identifier: "PointerAddress.FromHex", + description: "Transforms raw hex string to PointerAddress" +}) /** * Encode a number as a variable length integer following the Cardano ledger specification @@ -130,7 +135,7 @@ export const FromHex = Schema.compose( * @category encoding/decoding */ export const encodeVariableLength = (natural: Natural.Natural) => - Effect.gen(function* () { + Eff.gen(function* () { // Handle the simple case: values less than 128 (0x80, binary 10000000) fit in a single byte // with no continuation bit needed if (natural < 128) { @@ -165,8 +170,8 @@ export const encodeVariableLength = (natural: Natural.Natural) => export const decodeVariableLength: ( bytes: Uint8Array, offset?: number | undefined -) => Effect.Effect<[Natural.Natural, number], PointerAddressError | ParseResult.ParseIssue> = Effect.fnUntraced( - function* (bytes, offset = 0) { +) => Eff.Effect<[Natural.Natural, number], PointerAddressError | ParseResult.ParseIssue> = Eff.fnUntraced( + function* (bytes: Uint8Array, offset = 0) { // The accumulated decoded value let number = 0 @@ -215,8 +220,19 @@ export const decodeVariableLength: ( ) /** - * Check if two PointerAddress instances are equal. + * Smart constructor for creating PointerAddress instances * + * @since 2.0.0 + * @category constructors + */ +export const make = (props: { + networkId: NetworkId.NetworkId + paymentCredential: Credential.Credential + pointer: Pointer.Pointer +}): PointerAddress => new PointerAddress(props) + +/** + * Check if two PointerAddress instances are equal. * * @since 2.0.0 * @category equality @@ -233,30 +249,114 @@ export const equals = (a: PointerAddress, b: PointerAddress): boolean => { } /** - * Generate a random PointerAddress. + * FastCheck arbitrary for generating random PointerAddress instances * * @since 2.0.0 - * @category generators + * @category testing */ -export const generator = FastCheck.tuple( - NetworkId.generator, - Credential.generator, - Natural.generator, - Natural.generator, - Natural.generator +export const arbitrary = FastCheck.tuple( + NetworkId.arbitrary, + Credential.arbitrary, + FastCheck.integer({ min: 1, max: 1000000 }), + FastCheck.integer({ min: 1, max: 1000000 }), + FastCheck.integer({ min: 1, max: 1000000 }) ).map( ([networkId, paymentCredential, slot, txIndex, certIndex]) => - new PointerAddress({ + make({ networkId, paymentCredential, - pointer: Pointer.make(slot, txIndex, certIndex) + pointer: Pointer.make( + Natural.make(slot), + Natural.make(txIndex), + Natural.make(certIndex) + ) }) ) -export const Codec = _Codec.createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - PointerAddressError -) +/** + * Effect namespace for PointerAddress operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert bytes to PointerAddress using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromBytes = (bytes: Uint8Array) => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new PointerAddressError({ message: "Failed to decode from bytes", cause }) + ) + + /** + * Convert hex string to PointerAddress using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromHex = (hex: string) => + Eff.mapError( + Schema.decode(FromHex)(hex), + (cause) => new PointerAddressError({ message: "Failed to decode from hex", cause }) + ) + + /** + * Convert PointerAddress to bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toBytes = (address: PointerAddress) => + Eff.mapError( + Schema.encode(FromBytes)(address), + (cause) => new PointerAddressError({ message: "Failed to encode to bytes", cause }) + ) + + /** + * Convert PointerAddress to hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toHex = (address: PointerAddress) => + Eff.mapError( + Schema.encode(FromHex)(address), + (cause) => new PointerAddressError({ message: "Failed to encode to hex", cause }) + ) +} + +/** + * Convert bytes to PointerAddress (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromBytes = (bytes: Uint8Array): PointerAddress => Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert hex string to PointerAddress (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromHex = (hex: string): PointerAddress => Eff.runSync(Effect.fromHex(hex)) + +/** + * Convert PointerAddress to bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toBytes = (address: PointerAddress): Uint8Array => Eff.runSync(Effect.toBytes(address)) + +/** + * Convert PointerAddress to hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toHex = (address: PointerAddress): string => Eff.runSync(Effect.toHex(address)) diff --git a/packages/evolution/src/PolicyId.ts b/packages/evolution/src/PolicyId.ts index a6b759b2..89d78a7b 100644 --- a/packages/evolution/src/PolicyId.ts +++ b/packages/evolution/src/PolicyId.ts @@ -1,6 +1,5 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" -import { createEncoders } from "./Codec.js" import * as Hash28 from "./Hash28.js" /** @@ -25,7 +24,7 @@ export class PolicyIdError extends Data.TaggedError("PolicyIdError")<{ * @since 2.0.0 * @category schemas */ -export const PolicyId = pipe(Hash28.HexSchema, Schema.brand("PolicyId")).annotations({ +export const PolicyId = Hash28.HexSchema.pipe(Schema.brand("PolicyId")).annotations({ identifier: "PolicyId" }) @@ -57,6 +56,14 @@ export const FromHex = Schema.compose( identifier: "PolicyId.Hex" }) +/** + * Smart constructor for PolicyId that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = PolicyId.make + /** * Check if two PolicyId instances are equal. * @@ -66,26 +73,140 @@ export const FromHex = Schema.compose( export const equals = (a: PolicyId, b: PolicyId): boolean => a === b /** - * Generate a random PolicyId. + * Check if the given value is a valid PolicyId + * + * @since 2.0.0 + * @category predicates + */ +export const isPolicyId = Schema.is(PolicyId) + +/** + * FastCheck arbitrary for generating random PolicyId instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Hash28.HASH28_BYTES_LENGTH, - maxLength: Hash28.HASH28_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.hexaString({ + minLength: Hash28.HEX_LENGTH, + maxLength: Hash28.HEX_LENGTH +}).map((hex) => hex as PolicyId) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for PolicyId encoding and decoding operations. + * Parse PolicyId from bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - PolicyIdError -) +export const fromBytes = (bytes: Uint8Array): PolicyId => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse PolicyId from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): PolicyId => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode PolicyId to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (policyId: PolicyId): Uint8Array => + Eff.runSync(Effect.toBytes(policyId)) + +/** + * Encode PolicyId to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (policyId: PolicyId): string => + Eff.runSync(Effect.toHex(policyId)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse PolicyId from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new PolicyIdError({ + message: "Failed to parse PolicyId from bytes", + cause + }) + ) + ) + + /** + * Parse PolicyId from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new PolicyIdError({ + message: "Failed to parse PolicyId from hex", + cause + }) + ) + ) + + /** + * Encode PolicyId to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (policyId: PolicyId): Eff.Effect => + Schema.encode(FromBytes)(policyId).pipe( + Eff.mapError( + (cause) => + new PolicyIdError({ + message: "Failed to encode PolicyId to bytes", + cause + }) + ) + ) + + /** + * Encode PolicyId to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (policyId: PolicyId): Eff.Effect => + Schema.encode(FromHex)(policyId).pipe( + Eff.mapError( + (cause) => + new PolicyIdError({ + message: "Failed to encode PolicyId to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/PoolKeyHash.ts b/packages/evolution/src/PoolKeyHash.ts index 202371e5..a3abc0ee 100644 --- a/packages/evolution/src/PoolKeyHash.ts +++ b/packages/evolution/src/PoolKeyHash.ts @@ -1,6 +1,5 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, pipe, Schema } from "effect" -import { createEncoders } from "./Codec.js" import * as Hash28 from "./Hash28.js" /** @@ -41,6 +40,14 @@ export const FromHex = Schema.compose( identifier: "PoolKeyHash.Hex" }) +/** + * Smart constructor for PoolKeyHash that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = PoolKeyHash.make + /** * Check if two PoolKeyHash instances are equal. * @@ -50,26 +57,111 @@ export const FromHex = Schema.compose( export const equals = (a: PoolKeyHash, b: PoolKeyHash): boolean => a === b /** - * Generate a random PoolKeyHash. + * FastCheck arbitrary for generating random PoolKeyHash instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Hash28.HASH28_BYTES_LENGTH, - maxLength: Hash28.HASH28_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.uint8Array({ + minLength: Hash28.BYTES_LENGTH, + maxLength: Hash28.BYTES_LENGTH +}).map((bytes) => Eff.runSync(Effect.fromBytes(bytes))) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for PoolKeyHash encoding and decoding operations. + * Parse PoolKeyHash from raw bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - PoolKeyHashError -) +export const fromBytes = (bytes: Uint8Array): PoolKeyHash => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse PoolKeyHash from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): PoolKeyHash => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode PoolKeyHash to raw bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (poolKeyHash: PoolKeyHash): Uint8Array => + Eff.runSync(Effect.toBytes(poolKeyHash)) + +/** + * Encode PoolKeyHash to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (poolKeyHash: PoolKeyHash): string => poolKeyHash // Already a hex string + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse PoolKeyHash from raw bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => + new PoolKeyHashError({ + message: "Failed to parse PoolKeyHash from bytes", + cause + }) + ) + + /** + * Parse PoolKeyHash from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Eff.mapError( + Schema.decode(PoolKeyHash)(hex), + (cause) => + new PoolKeyHashError({ + message: "Failed to parse PoolKeyHash from hex", + cause + }) + ) + + /** + * Encode PoolKeyHash to raw bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (poolKeyHash: PoolKeyHash): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(poolKeyHash), + (cause) => + new PoolKeyHashError({ + message: "Failed to encode PoolKeyHash to bytes", + cause + }) + ) +} diff --git a/packages/evolution/src/PoolMetadata.ts b/packages/evolution/src/PoolMetadata.ts index 06ddc28d..f0352a46 100644 --- a/packages/evolution/src/PoolMetadata.ts +++ b/packages/evolution/src/PoolMetadata.ts @@ -1,8 +1,7 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as Url from "./Url.js" /** @@ -13,7 +12,7 @@ import * as Url from "./Url.js" */ export class PoolMetadataError extends Data.TaggedError("PoolMetadataError")<{ message?: string - reason?: "InvalidStructure" | "InvalidUrl" | "InvalidBytes" + cause?: unknown }> {} /** @@ -45,14 +44,48 @@ export const FromCDDL = Schema.transformOrFail( Schema.typeSchema(PoolMetadata), { strict: true, - encode: (poolMetadata) => Effect.succeed([poolMetadata.url, poolMetadata.hash] as const), + encode: (poolMetadata) => Eff.succeed([poolMetadata.url, poolMetadata.hash] as const), decode: ([urlText, hash]) => - Effect.gen(function* () { + Eff.gen(function* () { const url = yield* ParseResult.decode(Url.Url)(urlText) return new PoolMetadata({ url, hash }) }) } -) +).annotations({ + identifier: "PoolMetadata.FromCDDL", + description: "Transforms CBOR structure to PoolMetadata" +}) + +/** + * Smart constructor for creating PoolMetadata instances + * + * @since 2.0.0 + * @category constructors + */ +export const make = (props: { + url: Url.Url + hash: Uint8Array +}): PoolMetadata => new PoolMetadata(props) + +/** + * Check if two PoolMetadata instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: PoolMetadata, b: PoolMetadata): boolean => + a.url === b.url && a.hash.every((byte, index) => byte === b.hash[index]) + +/** + * FastCheck arbitrary for generating random PoolMetadata instances + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary = FastCheck.record({ + url: Url.arbitrary, + hash: FastCheck.uint8Array({ minLength: 32, maxLength: 32 }) +}).map(make) /** * CBOR bytes transformation schema for PoolMetadata. @@ -61,11 +94,14 @@ export const FromCDDL = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → PoolMetadata - ) + ).annotations({ + identifier: "PoolMetadata.FromCBORBytes", + description: "Transforms CBOR bytes to PoolMetadata" + }) /** * CBOR hex transformation schema for PoolMetadata. @@ -74,17 +110,103 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → PoolMetadata - ) - -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - PoolMetadataError - ) + FromCBORBytes(options) // Uint8Array → PoolMetadata + ).annotations({ + identifier: "PoolMetadata.FromCBORHex", + description: "Transforms CBOR hex string to PoolMetadata" + }) + +/** + * Effect namespace for PoolMetadata operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert CBOR bytes to PoolMetadata using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (cause) => new PoolMetadataError({ message: "Failed to decode from CBOR bytes", cause }) + ) + + /** + * Convert CBOR hex string to PoolMetadata using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (cause) => new PoolMetadataError({ message: "Failed to decode from CBOR hex", cause }) + ) + + /** + * Convert PoolMetadata to CBOR bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORBytes = (metadata: PoolMetadata, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(metadata), + (cause) => new PoolMetadataError({ message: "Failed to encode to CBOR bytes", cause }) + ) + + /** + * Convert PoolMetadata to CBOR hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORHex = (metadata: PoolMetadata, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(metadata), + (cause) => new PoolMetadataError({ message: "Failed to encode to CBOR hex", cause }) + ) +} + +/** + * Convert CBOR bytes to PoolMetadata (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): PoolMetadata => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Convert CBOR hex string to PoolMetadata (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): PoolMetadata => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Convert PoolMetadata to CBOR bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORBytes = (metadata: PoolMetadata, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(metadata, options)) + +/** + * Convert PoolMetadata to CBOR hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORHex = (metadata: PoolMetadata, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(metadata, options)) diff --git a/packages/evolution/src/PoolParams.ts b/packages/evolution/src/PoolParams.ts index a46ab334..22206f71 100644 --- a/packages/evolution/src/PoolParams.ts +++ b/packages/evolution/src/PoolParams.ts @@ -1,8 +1,7 @@ -import { BigDecimal, Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { BigDecimal, Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as Coin from "./Coin.js" import * as KeyHash from "./KeyHash.js" import * as NetworkId from "./NetworkId.js" @@ -47,8 +46,8 @@ export class PoolParamsError extends Data.TaggedError("PoolParamsError")<{ export class PoolParams extends Schema.TaggedClass()("PoolParams", { operator: PoolKeyHash.PoolKeyHash, vrfKeyhash: VrfKeyHash.VrfKeyHash, - pledge: Coin.CoinSchema, - cost: Coin.CoinSchema, + pledge: Coin.Coin, + cost: Coin.Coin, margin: UnitInterval.UnitInterval, rewardAccount: RewardAccount.RewardAccount, poolOwners: Schema.Array(KeyHash.KeyHash), @@ -56,22 +55,19 @@ export class PoolParams extends Schema.TaggedClass()("PoolParams", { poolMetadata: Schema.optionalWith(PoolMetadata.PoolMetadata, { nullable: true }) -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: "PoolParams", - operator: this.operator, - vrfKeyhash: this.vrfKeyhash, - pledge: this.pledge, - cost: this.cost, - margin: this.margin, - rewardAccount: this.rewardAccount, - poolOwners: this.poolOwners, - relays: this.relays, - poolMetadata: this.poolMetadata - } - } -} +}) {} + +export const CDDLSchema = Schema.Tuple( + CBOR.ByteArray, // operator (pool_keyhash as bytes) + CBOR.ByteArray, // vrf_keyhash (as bytes) + CBOR.Integer, // pledge (coin) + CBOR.Integer, // cost (coin) + UnitInterval.CDDLSchema, // margin using UnitInterval CDDL schema + CBOR.ByteArray, // reward_account (bytes) + Schema.Array(CBOR.ByteArray), // pool_owners (set as bytes array) + Schema.Array(Schema.encodedSchema(Relay.FromCDDL)), // relays using Relay CDDL schema + Schema.NullOr(Schema.encodedSchema(PoolMetadata.FromCDDL)) // pool_metadata using PoolMetadata CDDL schema +) /** * CDDL schema for PoolParams. @@ -93,95 +89,82 @@ export class PoolParams extends Schema.TaggedClass()("PoolParams", { * @since 2.0.0 * @category schemas */ -export const PoolParamsCDDLSchema = Schema.transformOrFail( - Schema.Tuple( - Schema.Uint8ArrayFromSelf, // operator (pool_keyhash as bytes) - Schema.Uint8ArrayFromSelf, // vrf_keyhash (as bytes) - Schema.BigIntFromSelf, // pledge (coin) - Schema.BigIntFromSelf, // cost (coin) - Schema.encodedSchema(UnitInterval.FromCDDL), // margin using UnitInterval CDDL schema - Schema.Uint8ArrayFromSelf, // reward_account (bytes) - Schema.Array(Schema.Uint8ArrayFromSelf), // pool_owners (set as bytes array) - Schema.Array(Schema.encodedSchema(Relay.FromCDDL)), // relays using Relay CDDL schema - Schema.NullOr(Schema.encodedSchema(PoolMetadata.FromCDDL)) // pool_metadata using PoolMetadata CDDL schema - ), - Schema.typeSchema(PoolParams), - { - strict: true, - encode: (toA) => - Effect.gen(function* () { - const operatorBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.operator) - const vrfKeyhashBytes = yield* ParseResult.encode(VrfKeyHash.FromBytes)(toA.vrfKeyhash) - const marginEncoded = yield* ParseResult.encode(UnitInterval.FromCDDL)(toA.margin) - const rewardAccountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(toA.rewardAccount) - - const poolOwnersBytes = yield* Effect.all( - toA.poolOwners.map((owner) => ParseResult.encode(KeyHash.FromBytes)(owner)) - ) - - const relaysEncoded = yield* Effect.all(toA.relays.map((relay) => ParseResult.encode(Relay.FromCDDL)(relay))) - - const poolMetadataEncoded = toA.poolMetadata - ? yield* ParseResult.encode(PoolMetadata.FromCDDL)(toA.poolMetadata) - : null - - return yield* Effect.succeed([ - operatorBytes, - vrfKeyhashBytes, - toA.pledge, - toA.cost, - marginEncoded, - rewardAccountBytes, - poolOwnersBytes, - relaysEncoded, - poolMetadataEncoded - ] as const) - }), - decode: ([ - operatorBytes, - vrfKeyhashBytes, - pledge, - cost, - marginEncoded, - rewardAccountBytes, - poolOwnersBytes, - relaysEncoded, - poolMetadataEncoded - ]) => - Effect.gen(function* () { - const operator = yield* ParseResult.decode(PoolKeyHash.FromBytes)(operatorBytes) - const vrfKeyhash = yield* ParseResult.decode(VrfKeyHash.FromBytes)(vrfKeyhashBytes) - const margin = yield* ParseResult.decode(UnitInterval.FromCDDL)(marginEncoded) - const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(rewardAccountBytes) - - const poolOwners = yield* Effect.all( - poolOwnersBytes.map((ownerBytes) => ParseResult.decode(KeyHash.FromBytes)(ownerBytes)) - ) - - const relays = yield* Effect.all( - relaysEncoded.map((relayEncoded) => ParseResult.decode(Relay.FromCDDL)(relayEncoded)) - ) - - const poolMetadata = poolMetadataEncoded - ? yield* ParseResult.decode(PoolMetadata.FromCDDL)(poolMetadataEncoded) - : undefined - - return yield* Effect.succeed( - new PoolParams({ - operator, - vrfKeyhash, - pledge, - cost, - margin, - rewardAccount, - poolOwners, - relays, - poolMetadata - }) - ) - }) - } -) +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(PoolParams), { + strict: true, + encode: (toA) => + Eff.gen(function* () { + const operatorBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.operator) + const vrfKeyhashBytes = yield* ParseResult.encode(VrfKeyHash.FromBytes)(toA.vrfKeyhash) + + const marginEncoded = yield* ParseResult.encode(UnitInterval.FromCDDL)(toA.margin) + const rewardAccountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(toA.rewardAccount) + + const poolOwnersBytes = yield* Eff.all( + toA.poolOwners.map((owner) => ParseResult.encode(KeyHash.FromBytes)(owner)) + ) + + const relaysEncoded = yield* Eff.all(toA.relays.map((relay) => ParseResult.encode(Relay.FromCDDL)(relay))) + + const poolMetadataEncoded = toA.poolMetadata + ? yield* ParseResult.encode(PoolMetadata.FromCDDL)(toA.poolMetadata) + : null + + return [ + operatorBytes, + vrfKeyhashBytes, + toA.pledge, + toA.cost, + marginEncoded, + rewardAccountBytes, + poolOwnersBytes, + relaysEncoded, + poolMetadataEncoded + ] as const + }), + decode: ([ + operatorBytes, + vrfKeyhashBytes, + pledge, + cost, + marginEncoded, + rewardAccountBytes, + poolOwnersBytes, + relaysEncoded, + poolMetadataEncoded + ]) => + Eff.gen(function* () { + const operator = yield* ParseResult.decode(PoolKeyHash.FromBytes)(operatorBytes) + const vrfKeyhash = yield* ParseResult.decode(VrfKeyHash.FromBytes)(vrfKeyhashBytes) + const margin = yield* ParseResult.decode(UnitInterval.FromCDDL)(marginEncoded) + const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(rewardAccountBytes) + + const poolOwners = yield* Eff.all( + poolOwnersBytes.map((ownerBytes) => ParseResult.decode(KeyHash.FromBytes)(ownerBytes)) + ) + + const relays = yield* Eff.all( + relaysEncoded.map((relayEncoded) => ParseResult.decode(Relay.FromCDDL)(relayEncoded)) + ) + + const poolMetadata = poolMetadataEncoded + ? yield* ParseResult.decode(PoolMetadata.FromCDDL)(poolMetadataEncoded) + : undefined + + return yield* Eff.succeed( + new PoolParams({ + operator, + vrfKeyhash, + pledge, + cost, + margin, + rewardAccount, + poolOwners, + relays, + poolMetadata + }) + ) + }) +}) /** * CBOR bytes transformation schema for PoolParams. @@ -189,10 +172,10 @@ export const PoolParamsCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - PoolParamsCDDLSchema // CBOR → PoolParams + FromCDDL // CBOR → PoolParams ) /** @@ -201,7 +184,7 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array FromBytes(options) // Uint8Array → PoolParams @@ -305,17 +288,17 @@ export const hasValidMargin = (params: PoolParams): boolean => params.margin.numerator <= params.margin.denominator && params.margin.denominator > 0n /** - * Generate a random PoolParams. + * FastCheck arbitrary for generating random PoolParams instances for testing. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.record({ - operator: PoolKeyHash.generator, - vrfKeyhash: VrfKeyHash.generator, +export const arbitrary = FastCheck.record({ + operator: PoolKeyHash.arbitrary, + vrfKeyhash: VrfKeyHash.arbitrary, pledge: FastCheck.bigInt({ min: 0n, max: 1000000000000n }), cost: FastCheck.bigInt({ min: 340000000n, max: 1000000000n }), - margin: UnitInterval.generator, + margin: UnitInterval.arbitrary, rewardAccount: FastCheck.constant( new RewardAccount.RewardAccount({ networkId: NetworkId.make(1), @@ -325,21 +308,144 @@ export const generator = FastCheck.record({ } }) ), - poolOwners: FastCheck.array(KeyHash.generator, { + poolOwners: FastCheck.array(KeyHash.arbitrary, { minLength: 1, maxLength: 5 }), - relays: FastCheck.array(Relay.generator, { minLength: 0, maxLength: 3 }), + relays: FastCheck.array(Relay.arbitrary, { minLength: 0, maxLength: 3 }), poolMetadata: FastCheck.option(FastCheck.constant(undefined), { nil: undefined }) }).map((params) => new PoolParams(params)) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - PoolParamsError - ) +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse PoolParams from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): PoolParams => + Eff.runSync(Effect.fromBytes(bytes, options)) + +/** + * Parse PoolParams from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string, options?: CBOR.CodecOptions): PoolParams => + Eff.runSync(Effect.fromHex(hex, options)) + +/** + * Encode PoolParams to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (poolParams: PoolParams, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toBytes(poolParams, options)) + +/** + * Encode PoolParams to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (poolParams: PoolParams, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toHex(poolParams, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse PoolParams from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new PoolParamsError({ + message: "Failed to parse PoolParams from bytes", + cause + }) + ) + ) + + /** + * Parse PoolParams from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new PoolParamsError({ + message: "Failed to parse PoolParams from hex", + cause + }) + ) + ) + + /** + * Encode PoolParams to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = ( + poolParams: PoolParams, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromBytes(options))(poolParams).pipe( + Eff.mapError( + (cause) => + new PoolParamsError({ + message: "Failed to encode PoolParams to bytes", + cause + }) + ) + ) + + /** + * Encode PoolParams to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = ( + poolParams: PoolParams, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromHex(options))(poolParams).pipe( + Eff.mapError( + (cause) => + new PoolParamsError({ + message: "Failed to encode PoolParams to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/Port.ts b/packages/evolution/src/Port.ts index 7947cfe4..75b4cd15 100644 --- a/packages/evolution/src/Port.ts +++ b/packages/evolution/src/Port.ts @@ -95,9 +95,9 @@ export const isDynamic = (port: Port): boolean => port >= 49152 && port <= 65535 * Generate a random Port. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = Numeric.Uint16Generator +export const arbitrary = Numeric.Uint16Generator /** * Synchronous encoding/decoding utilities. diff --git a/packages/evolution/src/PositiveCoin.ts b/packages/evolution/src/PositiveCoin.ts index 954d2ebb..2f738603 100644 --- a/packages/evolution/src/PositiveCoin.ts +++ b/packages/evolution/src/PositiveCoin.ts @@ -1,4 +1,4 @@ -import { Data, FastCheck, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Coin from "./Coin.js" @@ -10,7 +10,7 @@ import * as Coin from "./Coin.js" */ export class PositiveCoinError extends Data.TaggedError("PositiveCoinError")<{ message?: string - reason?: "InvalidAmount" | "ZeroAmount" | "ExceedsMaxValue" + cause?: unknown }> {} /** @@ -37,11 +37,13 @@ export const MAX_POSITIVE_COIN_VALUE = Coin.MAX_COIN_VALUE * @category schemas */ export const PositiveCoinSchema = Schema.BigIntFromSelf.pipe( - Schema.filter((value) => value >= MIN_POSITIVE_COIN_VALUE && value <= MAX_POSITIVE_COIN_VALUE) + Schema.filter((value) => value >= MIN_POSITIVE_COIN_VALUE && value <= MAX_POSITIVE_COIN_VALUE), ).annotations({ message: (issue) => `PositiveCoin must be between ${MIN_POSITIVE_COIN_VALUE} and ${MAX_POSITIVE_COIN_VALUE}, but got ${issue.actual}`, - identifier: "PositiveCoin" + identifier: "PositiveCoin", + title: "Positive Coin Amount", + description: "A positive amount of native assets (1 to maxWord64)" }) /** @@ -55,11 +57,12 @@ export type PositiveCoin = typeof PositiveCoinSchema.Type /** * Smart constructor for creating PositiveCoin values. + * Uses the built-in .make property for branded schemas. * * @since 2.0.0 * @category constructors */ -export const make = (value: bigint): PositiveCoin => PositiveCoinSchema.make(value) +export const make = PositiveCoinSchema.make /** * Create a PositiveCoin from a regular Coin, throwing if the value is zero. @@ -70,8 +73,7 @@ export const make = (value: bigint): PositiveCoin => PositiveCoinSchema.make(val export const fromCoin = (coin: Coin.Coin): PositiveCoin => { if (coin === 0n) { throw new PositiveCoinError({ - message: "Cannot create PositiveCoin from zero coin amount", - reason: "ZeroAmount" + message: "Cannot create PositiveCoin from zero coin amount" }) } return make(coin) @@ -103,11 +105,10 @@ export const add = (a: PositiveCoin, b: PositiveCoin): PositiveCoin => { const result = a + b if (result > MAX_POSITIVE_COIN_VALUE) { throw new PositiveCoinError({ - message: `Addition overflow: ${a} + ${b} exceeds maximum positive coin value`, - reason: "ExceedsMaxValue" + message: `Addition overflow: ${a} + ${b} exceeds maximum positive coin value` }) } - return result + return make(result) } /** @@ -121,11 +122,10 @@ export const subtract = (a: PositiveCoin, b: PositiveCoin): PositiveCoin => { const result = a - b if (result <= 0n) { throw new PositiveCoinError({ - message: `Subtraction underflow: ${a} - ${b} results in non-positive value`, - reason: "ZeroAmount" + message: `Subtraction underflow: ${a} - ${b} results in non-positive value` }) } - return result + return make(result) } /** @@ -149,40 +149,82 @@ export const compare = (a: PositiveCoin, b: PositiveCoin): -1 | 0 | 1 => { export const equals = (a: PositiveCoin, b: PositiveCoin): boolean => a === b /** - * Generate a random PositiveCoin value. + * FastCheck arbitrary for generating random PositiveCoin values. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.bigInt({ +export const arbitrary = FastCheck.bigInt({ min: MIN_POSITIVE_COIN_VALUE, max: MAX_POSITIVE_COIN_VALUE -}) +}).map(make) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Synchronous encoding/decoding utilities. + * Parse PositiveCoin from bigint value. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Encode = { - sync: Schema.encodeSync(PositiveCoinSchema) -} +export const fromBigInt = (value: bigint): PositiveCoin => + Eff.runSync(Effect.fromBigInt(value)) -export const Decode = { - sync: Schema.decodeUnknownSync(PositiveCoinSchema) -} +/** + * Convert PositiveCoin to bigint value. + * + * @since 2.0.0 + * @category encoding + */ +export const toBigInt = (positiveCoin: PositiveCoin): bigint => + Eff.runSync(Effect.toBigInt(positiveCoin)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ /** - * Either encoding/decoding utilities. + * Effect-based error handling variants for functions that can fail. * * @since 2.0.0 - * @category encoding/decoding + * @category effect */ -export const EncodeEither = { - either: Schema.encodeEither(PositiveCoinSchema) -} +export namespace Effect { + /** + * Parse PositiveCoin from bigint value with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBigInt = (value: bigint): Eff.Effect => + Schema.decode(PositiveCoinSchema)(value).pipe( + Eff.mapError( + (cause) => + new PositiveCoinError({ + message: "Failed to parse PositiveCoin from bigint", + cause + }) + ) + ) -export const DecodeEither = { - either: Schema.decodeUnknownEither(PositiveCoinSchema) + /** + * Convert PositiveCoin to bigint value with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBigInt = (positiveCoin: PositiveCoin): Eff.Effect => + Schema.encode(PositiveCoinSchema)(positiveCoin).pipe( + Eff.mapError( + (cause) => + new PositiveCoinError({ + message: "Failed to encode PositiveCoin to bigint", + cause + }) + ) + ) } + +// ============================================================================ diff --git a/packages/evolution/src/PrivateKey.ts b/packages/evolution/src/PrivateKey.ts new file mode 100644 index 00000000..90843b9e --- /dev/null +++ b/packages/evolution/src/PrivateKey.ts @@ -0,0 +1,423 @@ +import { bech32 } from "@scure/base" +import * as BIP32 from "@scure/bip32" +import * as BIP39 from "@scure/bip39" +import { wordlist } from "@scure/bip39/wordlists/english" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" +import sodium from "libsodium-wrappers-sumo" + +import { Bytes32, Bytes64, VKey } from "./index.js" + +/** + * Error class for PrivateKey related operations. + * + * @since 2.0.0 + * @category errors + */ +export class PrivateKeyError extends Data.TaggedError("PrivateKeyError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Schema for PrivateKey representing an Ed25519 private key. + * Supports both standard 32-byte and CIP-0003 extended 64-byte formats. + * Follows the Conway-era CDDL specification with CIP-0003 compatibility. + * + * @since 2.0.0 + * @category schemas + */ +export const PrivateKey = Schema.Union(Bytes32.HexSchema, Bytes64.HexSchema) + .pipe(Schema.brand("PrivateKey")) + .annotations({ + identifier: "PrivateKey", + description: "An Ed25519 private key supporting both standard 32-byte and CIP-0003 extended 64-byte formats" + }) + +export type PrivateKey = typeof PrivateKey.Type + +export const FromBytes = Schema.compose(Schema.Union(Bytes32.FromBytes, Bytes64.FromBytes), PrivateKey).annotations({ + identifier: "PrivateKey.FromBytes", + description: "Transforms raw bytes (Uint8Array) to PrivateKey hex string" +}) + +export const FromHex = PrivateKey + +export const FromBech32 = Schema.transformOrFail(Schema.String, PrivateKey, { + strict: true, + encode: (_, __, ___, toA) => + Eff.gen(function* () { + const privateKeyBytes = yield* ParseResult.encode(FromBytes)(toA) + const words = bech32.toWords(privateKeyBytes) + return bech32.encode("ed25519e_sk", words, 1023) + }), + decode: (fromA, _, ast) => + Eff.gen(function* () { + const { prefix, words } = yield* ParseResult.try({ + try: () => bech32.decode(fromA as any, 1023), + catch: (error) => new ParseResult.Type(ast, fromA, `Failed to decode Bech32: ${(error as Error).message}`) + }) + if (prefix !== "ed25519e_sk") { + throw new ParseResult.Type(ast, fromA, `Expected ed25519e_sk prefix, got ${prefix}`) + } + const decoded = bech32.fromWords(words) + return yield* ParseResult.decode(FromBytes)(decoded) + }) +}).annotations({ + identifier: "PrivateKey.FromBech32", + description: "Transforms Bech32 string (ed25519e_sk1...) to PrivateKey" +}) + +/** + * Smart constructor for PrivateKey that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = PrivateKey.make + +/** + * Check if two PrivateKey instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: PrivateKey, b: PrivateKey): boolean => a === b + +/** + * FastCheck arbitrary for generating random PrivateKey instances. + * Generates 32-byte private keys. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.uint8Array({ minLength: 32, maxLength: 32 }).map((bytes) => + Eff.runSync(Effect.fromBytes(bytes)) +) + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse a PrivateKey from raw bytes. + * Supports both 32-byte and 64-byte private keys. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): PrivateKey => Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse a PrivateKey from a hex string. + * Supports both 32-byte (64 chars) and 64-byte (128 chars) hex strings. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): PrivateKey => Eff.runSync(Effect.fromHex(hex)) + +/** + * Parse a PrivateKey from a Bech32 string. + * Expected format: ed25519e_sk1... + * + * @since 2.0.0 + * @category parsing + */ +export const fromBech32 = (bech32: string): PrivateKey => Eff.runSync(Effect.fromBech32(bech32)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert a PrivateKey to raw bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (privateKey: PrivateKey): Uint8Array => Eff.runSync(Effect.toBytes(privateKey)) + +/** + * Convert a PrivateKey to a hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (privateKey: PrivateKey): string => privateKey // Already a hex string + +/** + * Convert a PrivateKey to a Bech32 string. + * Format: ed25519e_sk1... + * + * @since 2.0.0 + * @category encoding + */ +export const toBech32 = (privateKey: PrivateKey): string => Eff.runSync(Effect.toBech32(privateKey)) + +// ============================================================================ +// Factory Functions +// ============================================================================ + +/** + * Generate a random 32-byte Ed25519 private key. + * Compatible with CML.PrivateKey.generate_ed25519(). + * + * @since 2.0.0 + * @category generators + */ +export const generate = () => sodium.randombytes_buf(32) + +/** + * Generate a random 64-byte extended Ed25519 private key. + * Compatible with CML.PrivateKey.generate_ed25519extended(). + * + * @since 2.0.0 + * @category generators + */ +export const generateExtended = () => sodium.randombytes_buf(64) + +/** + * Derive the public key (VKey) from a private key. + * Compatible with CML privateKey.to_public(). + * + * @since 2.0.0 + * @category cryptography + */ +export const toPublicKey = (privateKey: PrivateKey): VKey.VKey => VKey.fromPrivateKey(privateKey) + +/** + * Generate a new mnemonic phrase using BIP39. + * + * @since 2.0.0 + * @category bip39 + */ +export const generateMnemonic = (strength: 128 | 160 | 192 | 224 | 256 = 256): string => + BIP39.generateMnemonic(wordlist, strength) + +/** + * Validate a mnemonic phrase using BIP39. + * + * @since 2.0.0 + * @category bip39 + */ +export const validateMnemonic = (mnemonic: string): boolean => BIP39.validateMnemonic(mnemonic, wordlist) + +/** + * Create a PrivateKey from a mnemonic phrase (sync version that throws PrivateKeyError). + * All errors are normalized to PrivateKeyError with contextual information. + * + * @since 2.0.0 + * @category bip39 + */ +export const fromMnemonic = (mnemonic: string, password?: string): PrivateKey => + Eff.runSync(Effect.fromMnemonic(mnemonic, password)) + +/** + * Derive a child private key using BIP32 path (sync version that throws PrivateKeyError). + * All errors are normalized to PrivateKeyError with contextual information. + * + * @since 2.0.0 + * @category bip32 + */ +export const derive = (privateKey: PrivateKey, path: string): PrivateKey => Eff.runSync(Effect.derive(privateKey, path)) + +// ============================================================================ +// Cryptographic Operations +// ============================================================================ + +/** + * Sign a message using Ed25519 (sync version that throws PrivateKeyError). + * All errors are normalized to PrivateKeyError with contextual information. + * For extended keys (64 bytes), uses CML-compatible Ed25519-BIP32 signing. + * For normal keys (32 bytes), uses standard Ed25519 signing. + * + * @since 2.0.0 + * @category cryptography + */ +export const sign = (privateKey: PrivateKey, message: Uint8Array): Uint8Array => + Eff.runSync(Effect.sign(privateKey, message)) + +/** + * Cardano BIP44 derivation path utilities. + * + * @since 2.0.0 + * @category cardano + */ +export const CardanoPath = { + /** + * Create a Cardano BIP44 derivation path. + * Standard path: m/1852'/1815'/account'/role/index + */ + create: (account: number = 0, role: 0 | 2 = 0, index: number = 0) => `m/1852'/${1815}'/${account}'/${role}/${index}`, + + /** + * Payment key path (role = 0) + */ + payment: (account: number = 0, index: number = 0) => CardanoPath.create(account, 0, index), + + /** + * Stake key path (role = 2) + */ + stake: (account: number = 0, index: number = 0) => CardanoPath.create(account, 2, index) +} + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse a PrivateKey from raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => + new PrivateKeyError({ + message: `Failed to parse PrivateKey from bytes`, + cause + }) + ) + + /** + * Parse a PrivateKey from a hex string using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Eff.mapError( + Schema.decode(FromHex)(hex), + (cause) => + new PrivateKeyError({ + message: "Failed to parse PrivateKey from hex", + cause + }) + ) + + /** + * Parse a PrivateKey from a Bech32 string using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBech32 = (bech32: string): Eff.Effect => + Eff.mapError( + Schema.decode(FromBech32)(bech32), + (cause) => + new PrivateKeyError({ + message: "Failed to parse PrivateKey from Bech32", + cause + }) + ) + + /** + * Convert a PrivateKey to raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (privateKey: PrivateKey): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(privateKey), + (cause) => + new PrivateKeyError({ + message: "Failed to encode PrivateKey to bytes", + cause + }) + ) + + /** + * Convert a PrivateKey to a Bech32 string using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBech32 = (privateKey: PrivateKey): Eff.Effect => + Eff.mapError( + Schema.encode(FromBech32)(privateKey), + (cause) => + new PrivateKeyError({ + message: "Failed to encode PrivateKey to Bech32", + cause + }) + ) + + /** + * Create a PrivateKey from a mnemonic phrase using Effect error handling. + * + * @since 2.0.0 + * @category bip39 + */ + export const fromMnemonic = (mnemonic: string, password?: string): Eff.Effect => + Eff.gen(function* () { + if (!validateMnemonic(mnemonic)) { + return yield* Eff.fail(new PrivateKeyError({ message: "Invalid mnemonic phrase" })) + } + const seed = BIP39.mnemonicToSeedSync(mnemonic, password || "") + const hdKey = BIP32.HDKey.fromMasterSeed(seed) + if (!hdKey.privateKey) { + return yield* Eff.fail(new PrivateKeyError({ message: "No private key in HD key" })) + } + return yield* fromBytes(hdKey.privateKey) + }) + + /** + * Derive a child private key using BIP32 path with Effect error handling. + * + * @since 2.0.0 + * @category bip32 + */ + export const derive = (privateKey: PrivateKey, path: string): Eff.Effect => + Eff.gen(function* () { + const privateKeyBytes = yield* toBytes(privateKey) + const hdKey = BIP32.HDKey.fromMasterSeed(privateKeyBytes) + const childKey = hdKey.derive(path) + if (!childKey.privateKey) { + return yield* Eff.fail(new PrivateKeyError({ message: "No private key in derived HD key" })) + } + return yield* fromBytes(childKey.privateKey) + }) + + /** + * Sign a message using Ed25519 with Effect error handling. + * + * @since 2.0.0 + * @category cryptography + */ + export const sign = (privateKey: PrivateKey, message: Uint8Array): Eff.Effect => + Eff.gen(function* () { + const privateKeyBytes = yield* toBytes(privateKey) + + if (privateKeyBytes.length === 64) { + // CML-compatible extended signing algorithm + const scalar = privateKeyBytes.slice(0, 32) + const iv = privateKeyBytes.slice(32, 64) + + const publicKey = sodium.crypto_scalarmult_ed25519_base_noclamp(scalar) + const nonceHash = sodium.crypto_hash_sha512(new Uint8Array([...iv, ...message])) + const nonce = sodium.crypto_core_ed25519_scalar_reduce(nonceHash) + const r = sodium.crypto_scalarmult_ed25519_base_noclamp(nonce) + const hramHash = sodium.crypto_hash_sha512(new Uint8Array([...r, ...publicKey, ...message])) + const hram = sodium.crypto_core_ed25519_scalar_reduce(hramHash) + const s = sodium.crypto_core_ed25519_scalar_add(sodium.crypto_core_ed25519_scalar_mul(hram, scalar), nonce) + + return new Uint8Array([...r, ...s]) + } + + // Standard 32-byte Ed25519 signing + const publicKey = sodium.crypto_sign_seed_keypair(privateKeyBytes).publicKey + const secretKey = new Uint8Array([...privateKeyBytes, ...publicKey]) + return sodium.crypto_sign_detached(message, secretKey) + }) +} diff --git a/packages/evolution/src/ProposalProcedures.ts b/packages/evolution/src/ProposalProcedures.ts index aeeebe2e..6b999f25 100644 --- a/packages/evolution/src/ProposalProcedures.ts +++ b/packages/evolution/src/ProposalProcedures.ts @@ -1,19 +1,364 @@ -import { Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" + +import * as Anchor from "./Anchor.js" +import * as Bytes from "./Bytes.js" +import * as CBOR from "./CBOR.js" +import * as Coin from "./Coin.js" +import * as GovernanceAction from "./GovernanceAction.js" +import * as RewardAccount from "./RewardAccount.js" + +/** + * Error class for ProposalProcedures related operations. + * + * @since 2.0.0 + * @category errors + */ +export class ProposalProceduresError extends Data.TaggedError("ProposalProceduresError")<{ + message?: string + cause?: unknown +}> {} /** - * ProposalProcedures based on Conway CDDL specification + * Schema for a single proposal procedure based on Conway CDDL specification. * * ``` - * CDDL: proposal_procedures = nonempty_set + * proposal_procedure = [ + * deposit : coin, + * reward_account : reward_account, + * governance_action : governance_action, + * anchor : anchor / null + * ] + * + * governance_action = [action_type, action_data] * ``` * - * This is a non-empty set of proposal procedures. + * @since 2.0.0 + * @category model + */ +export class ProposalProcedure extends Schema.Class("ProposalProcedure")({ + deposit: Coin.Coin, + rewardAccount: RewardAccount.RewardAccount, + governanceAction: GovernanceAction.GovernanceAction, + anchor: Schema.NullOr(Anchor.Anchor) +}) {} + +/** + * ProposalProcedures based on Conway CDDL specification. + * + * ``` + * CDDL: proposal_procedures = nonempty_set + * ``` * * @since 2.0.0 * @category model */ +export class ProposalProcedures extends Schema.Class("ProposalProcedures")({ + procedures: Schema.Array(ProposalProcedure).pipe( + Schema.filter((arr) => arr.length > 0, { + message: () => "ProposalProcedures must contain at least one procedure" + }) + ) +}) {} + +/** + * CDDL schema for ProposalProcedure tuple structure. + * + * @since 2.0.0 + * @category schemas + */ +export const ProposalProcedureCDDLSchema = Schema.Tuple( + CBOR.Integer, // deposit: coin + CBOR.ByteArray, // reward_account (raw bytes) + GovernanceAction.CDDLSchema, // governance_action using proper CDDL schema + Schema.NullOr(Anchor.CDDLSchema) // anchor / null +) + +/** + * CDDL schema for ProposalProcedures that produces CBOR-compatible types. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = Schema.Array(ProposalProcedureCDDLSchema) + +/** + * CDDL transformation schema for ProposalProcedures. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(ProposalProcedures), { + strict: true, + encode: (toA) => + Eff.all( + toA.procedures.map((procedure) => + Eff.gen(function* () { + const depositBigInt = BigInt(procedure.deposit) + const rewardAccountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(procedure.rewardAccount) + const governanceActionCDDL = yield* ParseResult.encode(GovernanceAction.FromCDDL)( + procedure.governanceAction + ) + const anchorCDDL = procedure.anchor ? yield* ParseResult.encode(Anchor.FromCDDL)(procedure.anchor) : null + return [depositBigInt, rewardAccountBytes, governanceActionCDDL, anchorCDDL] as any + }) + ) + ), + decode: (fromA) => + Eff.gen(function* () { + const procedures = yield* Eff.all( + fromA.map((procedureTuple: any) => + Eff.gen(function* () { + const [depositBigInt, rewardAccountBytes, governanceActionCDDL, anchorCDDL] = procedureTuple + const deposit = Coin.make(depositBigInt) + const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(rewardAccountBytes) + const governanceAction = yield* ParseResult.decode(GovernanceAction.FromCDDL)(governanceActionCDDL) + const anchor = anchorCDDL ? yield* ParseResult.decode(Anchor.FromCDDL)(anchorCDDL) : null + + return new ProposalProcedure({ + deposit, + rewardAccount, + governanceAction, + anchor + }) + }) + ) + ) + + return new ProposalProcedures({ procedures }) + }) +}) + +/** + * CBOR bytes transformation schema for ProposalProcedures. + * + * @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 → ProposalProcedures + ).annotations({ + identifier: "ProposalProcedures.FromCBORBytes", + title: "ProposalProcedures from CBOR Bytes", + description: "Transforms CBOR bytes to ProposalProcedures" + }) + +/** + * CBOR hex transformation schema for ProposalProcedures. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose( + Bytes.FromHex, // string → Uint8Array + FromCBORBytes(options) // Uint8Array → ProposalProcedures + ).annotations({ + identifier: "ProposalProcedures.FromCBORHex", + title: "ProposalProcedures from CBOR Hex", + description: "Transforms CBOR hex string to ProposalProcedures" + }) + +/** + * Check if two ProposalProcedures instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: ProposalProcedures, b: ProposalProcedures): boolean => + a.procedures.length === b.procedures.length && + a.procedures.every((procedureA, index) => { + const procedureB = b.procedures[index] + return ( + procedureA.deposit === procedureB.deposit && + RewardAccount.equals(procedureA.rewardAccount, procedureB.rewardAccount) && + GovernanceAction.equals(procedureA.governanceAction, procedureB.governanceAction) && + ((procedureA.anchor === null && procedureB.anchor === null) || + (procedureA.anchor !== null && + procedureB.anchor !== null && + Anchor.equals(procedureA.anchor, procedureB.anchor))) + ) + }) + +/** + * Create a ProposalProcedures instance with validation. + * + * @since 2.0.0 + * @category constructors + */ +export const make = (procedures: Array): ProposalProcedures => { + if (procedures.length === 0) { + throw new Error("ProposalProcedures must contain at least one procedure") + } + return new ProposalProcedures({ procedures }) +} + +/** + * Create a single ProposalProcedure. + * + * @since 2.0.0 + * @category constructors + */ +export const makeProcedure = (params: { + deposit: Coin.Coin + rewardAccount: RewardAccount.RewardAccount + governanceAction: GovernanceAction.GovernanceAction + anchor?: Anchor.Anchor | null +}): ProposalProcedure => + new ProposalProcedure({ + deposit: params.deposit, + rewardAccount: params.rewardAccount, + governanceAction: params.governanceAction, + anchor: params.anchor ?? null + }) + +/** + * FastCheck arbitrary for ProposalProcedures. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.record({ + procedures: FastCheck.array( + FastCheck.record({ + deposit: Coin.arbitrary, + rewardAccount: RewardAccount.arbitrary, + governanceAction: GovernanceAction.arbitrary, + anchor: FastCheck.option(Anchor.arbitrary, { nil: null }) + }).map((params) => new ProposalProcedure(params)), + { minLength: 1, maxLength: 5 } + ) +}).map((params) => new ProposalProcedures(params)) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse ProposalProcedures from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): ProposalProcedures => + Eff.runSync(Effect.fromCBORBytes(bytes, options) as any) + +/** + * Parse ProposalProcedures from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): ProposalProcedures => + Eff.runSync(Effect.fromCBORHex(hex, options) as any) + +/** + * Encode ProposalProcedures to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (proposalProcedures: ProposalProcedures, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(proposalProcedures, options) as any) + +/** + * Encode ProposalProcedures to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (proposalProcedures: ProposalProcedures, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(proposalProcedures, options) as any) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse ProposalProcedures from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new ProposalProceduresError({ + message: "Failed to parse ProposalProcedures from bytes", + cause + }) + ) + ) + + /** + * Parse ProposalProcedures from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new ProposalProceduresError({ + message: "Failed to parse ProposalProcedures from hex", + cause + }) + ) + ) -// TODO: Implement when ProposalProcedure is available -export const ProposalProcedures = Schema.Unknown + /** + * Encode ProposalProcedures to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = ( + proposalProcedures: ProposalProcedures, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORBytes(options))(proposalProcedures).pipe( + Eff.mapError( + (cause) => + new ProposalProceduresError({ + message: "Failed to encode ProposalProcedures to bytes", + cause + }) + ) + ) -export type ProposalProcedures = Schema.Schema.Type + /** + * Encode ProposalProcedures to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = ( + proposalProcedures: ProposalProcedures, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORHex(options))(proposalProcedures).pipe( + Eff.mapError( + (cause) => + new ProposalProceduresError({ + message: "Failed to encode ProposalProcedures to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/ProtocolVersion.ts b/packages/evolution/src/ProtocolVersion.ts index cbde9c1d..6d3a0ff4 100644 --- a/packages/evolution/src/ProtocolVersion.ts +++ b/packages/evolution/src/ProtocolVersion.ts @@ -1,8 +1,7 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as Numeric from "./Numeric.js" /** @@ -29,6 +28,20 @@ export class ProtocolVersion extends Schema.TaggedClass()("Prot minor: Numeric.Uint32Schema }) {} +/** + * Smart constructor for creating ProtocolVersion instances + * + * @since 2.0.0 + * @category constructors + */ +export const make = (props: { + major: number + minor: number +}): ProtocolVersion => new ProtocolVersion({ + major: Numeric.Uint32Make(props.major), + minor: Numeric.Uint32Make(props.minor) +}) + /** * Check if two ProtocolVersion instances are equal. * @@ -37,6 +50,17 @@ export class ProtocolVersion extends Schema.TaggedClass()("Prot */ export const equals = (a: ProtocolVersion, b: ProtocolVersion): boolean => a.major === b.major && a.minor === b.minor +/** + * FastCheck arbitrary for generating random ProtocolVersion instances + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary = FastCheck.tuple( + Numeric.Uint32Generator, + Numeric.Uint32Generator +).map(([major, minor]) => make({ major, minor })) + /** * CDDL schema for ProtocolVersion. * protocol_version = [major_version : uint32, minor_version : uint32] @@ -49,7 +73,7 @@ export const FromCDDL = Schema.transformOrFail( Schema.typeSchema(ProtocolVersion), { strict: true, - encode: (toA) => Effect.succeed([BigInt(toA.major), BigInt(toA.minor)] as const), + encode: (toA) => Eff.succeed([BigInt(toA.major), BigInt(toA.minor)] as const), decode: ([major, minor]) => ParseResult.decode(ProtocolVersion)({ _tag: "ProtocolVersion", @@ -57,7 +81,10 @@ export const FromCDDL = Schema.transformOrFail( minor: Number(minor) }) } -) +).annotations({ + identifier: "ProtocolVersion.FromCDDL", + description: "Transforms CBOR structure to ProtocolVersion" +}) /** * CBOR bytes transformation schema for ProtocolVersion. @@ -65,11 +92,14 @@ export const FromCDDL = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → ProtocolVersion - ) + ).annotations({ + identifier: "ProtocolVersion.FromCBORBytes", + description: "Transforms CBOR bytes to ProtocolVersion" + }) /** * CBOR hex transformation schema for ProtocolVersion. @@ -77,17 +107,103 @@ export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) * @since 2.0.0 * @category schemas */ -export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array FromCBORBytes(options) // Uint8Array → ProtocolVersion - ) - -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - bytes: FromCBORBytes(options), - variableBytes: FromCBORHex(options) - }, - ProtocolVersionError - ) + ).annotations({ + identifier: "ProtocolVersion.FromCBORHex", + description: "Transforms CBOR hex string to ProtocolVersion" + }) + +/** + * Effect namespace for ProtocolVersion operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert CBOR bytes to ProtocolVersion using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (cause) => new ProtocolVersionError({ message: "Failed to decode from CBOR bytes", cause }) + ) + + /** + * Convert CBOR hex string to ProtocolVersion using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (cause) => new ProtocolVersionError({ message: "Failed to decode from CBOR hex", cause }) + ) + + /** + * Convert ProtocolVersion to CBOR bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORBytes = (version: ProtocolVersion, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(version), + (cause) => new ProtocolVersionError({ message: "Failed to encode to CBOR bytes", cause }) + ) + + /** + * Convert ProtocolVersion to CBOR hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORHex = (version: ProtocolVersion, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(version), + (cause) => new ProtocolVersionError({ message: "Failed to encode to CBOR hex", cause }) + ) +} + +/** + * Convert CBOR bytes to ProtocolVersion (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): ProtocolVersion => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Convert CBOR hex string to ProtocolVersion (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): ProtocolVersion => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Convert ProtocolVersion to CBOR bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORBytes = (version: ProtocolVersion, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(version, options)) + +/** + * Convert ProtocolVersion to CBOR hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORHex = (version: ProtocolVersion, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(version, options)) diff --git a/packages/evolution/src/Relay.ts b/packages/evolution/src/Relay.ts index 3a768e55..1e5c5028 100644 --- a/packages/evolution/src/Relay.ts +++ b/packages/evolution/src/Relay.ts @@ -1,8 +1,7 @@ -import { Data, FastCheck, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as MultiHostName from "./MultiHostName.js" import * as SingleHostAddr from "./SingleHostAddr.js" import * as SingleHostName from "./SingleHostName.js" @@ -15,7 +14,7 @@ import * as SingleHostName from "./SingleHostName.js" */ export class RelayError extends Data.TaggedError("RelayError")<{ message?: string - reason?: "InvalidStructure" | "UnsupportedType" + cause?: unknown }> {} /** @@ -31,11 +30,7 @@ export const Relay = Schema.Union( MultiHostName.MultiHostName ) -export const FromCDDL = Schema.Union( - SingleHostAddr.SingleHostAddrCDDLSchema, - SingleHostName.SingleHostNameCDDLSchema, - MultiHostName.FromCDDL -) +export const FromCDDL = Schema.Union(SingleHostAddr.FromCDDL, SingleHostName.FromCDDL, MultiHostName.FromCDDL) /** * Type alias for Relay. @@ -47,14 +42,21 @@ export type Relay = typeof Relay.Type /** * CBOR bytes transformation schema for Relay. - * For union types, we create a union of the child FromBytess - * rather than trying to create a complex three-layer transformation. + * For union types, we create a union of the child CBOR schemas. * * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - Schema.Union(SingleHostAddr.FromBytes(options), SingleHostName.FromBytes(options), MultiHostName.FromBytes(options)) +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.Union( + SingleHostAddr.FromCBORBytes(options), + SingleHostName.FromBytes(options), // Still uses old naming + MultiHostName.FromCBORBytes(options) + ).annotations({ + identifier: "Relay.FromCBORBytes", + title: "Relay from CBOR Bytes", + description: "Transforms CBOR bytes (Uint8Array) to Relay" + }) /** * CBOR hex transformation schema for Relay. @@ -62,122 +64,211 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → Relay - ) - -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - RelayError - ) + FromCBORBytes(options) // Uint8Array → Relay + ).annotations({ + identifier: "Relay.FromCBORHex", + title: "Relay from CBOR Hex", + description: "Transforms CBOR hex string to Relay" + }) /** - * Pattern match on a Relay to handle different relay types. + * Check if two Relay instances are equal. * * @since 2.0.0 - * @category transformation + * @category equality */ -export const match = ( - relay: Relay, - cases: { - SingleHostAddr: (addr: SingleHostAddr.SingleHostAddr) => A - SingleHostName: (name: SingleHostName.SingleHostName) => B - MultiHostName: (multi: MultiHostName.MultiHostName) => C - } -): A | B | C => { - switch (relay._tag) { +export const equals = (self: Relay, that: Relay): boolean => { + if (self._tag !== that._tag) return false + + switch (self._tag) { case "SingleHostAddr": - return cases.SingleHostAddr(relay) + return SingleHostAddr.equals(self, that as SingleHostAddr.SingleHostAddr) case "SingleHostName": - return cases.SingleHostName(relay) + return SingleHostName.equals(self, that as SingleHostName.SingleHostName) case "MultiHostName": - return cases.MultiHostName(relay) + return MultiHostName.equals(self, that as MultiHostName.MultiHostName) default: - throw new Error(`Exhaustive check failed: Unhandled case '${(relay as { _tag: string })._tag}' encountered.`) + return false } } /** - * Check if a Relay is a SingleHostAddr. + * @since 2.0.0 + * @category FastCheck + */ +export const arbitrary = FastCheck.oneof( + SingleHostAddr.arbitrary, + FastCheck.constant({} as SingleHostName.SingleHostName), // Placeholder since it may not have arbitrary + MultiHostName.arbitrary +) + +/** + * Create a Relay from a SingleHostAddr. * * @since 2.0.0 - * @category predicates + * @category constructors */ -export const isSingleHostAddr = (relay: Relay): relay is SingleHostAddr.SingleHostAddr => - relay._tag === "SingleHostAddr" +export const fromSingleHostAddr = (singleHostAddr: SingleHostAddr.SingleHostAddr): Relay => singleHostAddr /** - * Check if a Relay is a SingleHostName. + * Create a Relay from a SingleHostName. * * @since 2.0.0 - * @category predicates + * @category constructors */ -export const isSingleHostName = (relay: Relay): relay is SingleHostName.SingleHostName => - relay._tag === "SingleHostName" +export const fromSingleHostName = (singleHostName: SingleHostName.SingleHostName): Relay => singleHostName /** - * Check if a Relay is a MultiHostName. + * Create a Relay from a MultiHostName. * * @since 2.0.0 - * @category predicates + * @category constructors */ -export const isMultiHostName = (relay: Relay): relay is MultiHostName.MultiHostName => relay._tag === "MultiHostName" +export const fromMultiHostName = (multiHostName: MultiHostName.MultiHostName): Relay => multiHostName /** - * FastCheck generator for Relay instances. + * Effect namespace containing schema decode and encode operations. * * @since 2.0.0 - * @category generators + * @category Effect */ -export const generator = FastCheck.oneof(SingleHostAddr.generator, SingleHostName.generator, MultiHostName.generator) +export namespace Effect { + /** + * Parse a Relay from CBOR bytes using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (input: Uint8Array, options?: CBOR.CodecOptions): Eff.Effect => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(input), + (cause) => new RelayError({ message: "Failed to decode Relay from CBOR bytes", cause }) + ) + + /** + * Parse a Relay from CBOR hex using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (input: string, options?: CBOR.CodecOptions): Eff.Effect => + Eff.mapError( + Schema.decode(FromCBORHex(options))(input), + (cause) => new RelayError({ message: "Failed to decode Relay from CBOR hex", cause }) + ) + + /** + * Convert a Relay to CBOR bytes using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (value: Relay, options?: CBOR.CodecOptions): Eff.Effect => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(value), + (cause) => new RelayError({ message: "Failed to encode Relay to CBOR bytes", cause }) + ) + + /** + * Convert a Relay to CBOR hex using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (value: Relay, options?: CBOR.CodecOptions): Eff.Effect => + Eff.mapError( + Schema.encode(FromCBORHex(options))(value), + (cause) => new RelayError({ message: "Failed to encode Relay to CBOR hex", cause }) + ) +} /** - * Check if two Relay instances are equal. + * Convert Relay to CBOR bytes (unsafe). * * @since 2.0.0 - * @category equality + * @category encoding */ -export const equals = (self: Relay, that: Relay): boolean => { - if (self._tag !== that._tag) return false +export const toCBORBytes = (value: Relay, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(value, options)) - switch (self._tag) { +/** + * Convert Relay to CBOR hex (unsafe). + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (value: Relay, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(value, options)) + +/** + * Parse Relay from CBOR bytes (unsafe). + * + * @since 2.0.0 + * @category decoding + */ +export const fromCBORBytes = (value: Uint8Array, options?: CBOR.CodecOptions): Relay => + Eff.runSync(Effect.fromCBORBytes(value, options)) + +/** + * Parse Relay from CBOR hex (unsafe). + * + * @since 2.0.0 + * @category decoding + */ +export const fromCBORHex = (value: string, options?: CBOR.CodecOptions): Relay => + Eff.runSync(Effect.fromCBORHex(value, options)) + +/** + * Pattern match on a Relay to handle different relay types. + * + * @since 2.0.0 + * @category transformation + */ +export const match = ( + relay: Relay, + cases: { + SingleHostAddr: (addr: SingleHostAddr.SingleHostAddr) => A + SingleHostName: (name: SingleHostName.SingleHostName) => B + MultiHostName: (multi: MultiHostName.MultiHostName) => C + } +): A | B | C => { + switch (relay._tag) { case "SingleHostAddr": - return SingleHostAddr.equals(self, that as SingleHostAddr.SingleHostAddr) + return cases.SingleHostAddr(relay) case "SingleHostName": - return SingleHostName.equals(self, that as SingleHostName.SingleHostName) + return cases.SingleHostName(relay) case "MultiHostName": - return MultiHostName.equals(self, that as MultiHostName.MultiHostName) + return cases.MultiHostName(relay) default: - return false + throw new Error(`Exhaustive check failed: Unhandled case '${(relay as { _tag: string })._tag}' encountered.`) } } /** - * Create a Relay from a SingleHostAddr. + * Check if a Relay is a SingleHostAddr. * * @since 2.0.0 - * @category constructors + * @category predicates */ -export const fromSingleHostAddr = (singleHostAddr: SingleHostAddr.SingleHostAddr): Relay => singleHostAddr +export const isSingleHostAddr = (relay: Relay): relay is SingleHostAddr.SingleHostAddr => + relay._tag === "SingleHostAddr" /** - * Create a Relay from a SingleHostName. + * Check if a Relay is a SingleHostName. * * @since 2.0.0 - * @category constructors + * @category predicates */ -export const fromSingleHostName = (singleHostName: SingleHostName.SingleHostName): Relay => singleHostName +export const isSingleHostName = (relay: Relay): relay is SingleHostName.SingleHostName => + relay._tag === "SingleHostName" /** - * Create a Relay from a MultiHostName. + * Check if a Relay is a MultiHostName. * * @since 2.0.0 - * @category constructors + * @category predicates */ -export const fromMultiHostName = (multiHostName: MultiHostName.MultiHostName): Relay => multiHostName +export const isMultiHostName = (relay: Relay): relay is MultiHostName.MultiHostName => relay._tag === "MultiHostName" diff --git a/packages/evolution/src/RewardAccount.ts b/packages/evolution/src/RewardAccount.ts index 9865f7cb..067a2f39 100644 --- a/packages/evolution/src/RewardAccount.ts +++ b/packages/evolution/src/RewardAccount.ts @@ -1,8 +1,7 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as Bytes29 from "./Bytes29.js" -import * as _Codec from "./Codec.js" import * as Credential from "./Credential.js" import * as KeyHash from "./KeyHash.js" import * as NetworkId from "./NetworkId.js" @@ -22,20 +21,12 @@ export class RewardAccountError extends Data.TaggedError("RewardAccountError")<{ export class RewardAccount extends Schema.TaggedClass("RewardAccount")("RewardAccount", { networkId: NetworkId.NetworkId, stakeCredential: Credential.Credential -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: "RewardAccount", - networkId: this.networkId, - stakeCredential: this.stakeCredential - } - } -} +}) {} export const FromBytes = Schema.transformOrFail(Bytes29.BytesSchema, RewardAccount, { strict: true, encode: (_, __, ___, toA) => - Effect.gen(function* () { + Eff.gen(function* () { const stakingBit = toA.stakeCredential._tag === "KeyHash" ? 0 : 1 const header = (0b111 << 5) | (stakingBit << 4) | (toA.networkId & 0b00001111) const result = new Uint8Array(29) @@ -45,7 +36,7 @@ export const FromBytes = Schema.transformOrFail(Bytes29.BytesSchema, RewardAccou return yield* ParseResult.succeed(result) }), decode: (_, __, ___, fromA) => - Effect.gen(function* () { + Eff.gen(function* () { const header = fromA[0] // Extract network ID from the lower 4 bits const networkId = header & 0b00001111 @@ -60,7 +51,7 @@ export const FromBytes = Schema.transformOrFail(Bytes29.BytesSchema, RewardAccou } : { _tag: "ScriptHash", - hash: yield* ParseResult.decode(ScriptHash.BytesSchema)(fromA.slice(1, 29)) + hash: yield* ParseResult.decode(ScriptHash.FromBytes)(fromA.slice(1, 29)) } return yield* ParseResult.decode(RewardAccount)({ _tag: "RewardAccount", @@ -68,12 +59,29 @@ export const FromBytes = Schema.transformOrFail(Bytes29.BytesSchema, RewardAccou stakeCredential }) }) +}).annotations({ + identifier: "RewardAccount.FromBytes", + description: "Transforms raw bytes to RewardAccount" }) export const FromHex = Schema.compose( Bytes.FromHex, // string → Uint8Array FromBytes // Uint8Array → RewardAccount -) +).annotations({ + identifier: "RewardAccount.FromHex", + description: "Transforms raw hex string to RewardAccount" +}) + +/** + * Smart constructor for creating RewardAccount instances + * + * @since 2.0.0 + * @category constructors + */ +export const make = (props: { + networkId: NetworkId.NetworkId + stakeCredential: Credential.Credential +}): RewardAccount => new RewardAccount(props) /** * Check if two RewardAccount instances are equal. @@ -90,23 +98,103 @@ export const equals = (a: RewardAccount, b: RewardAccount): boolean => { } /** - * Generate a random RewardAccount. + * FastCheck arbitrary for generating random RewardAccount instances * * @since 2.0.0 - * @category generators + * @category testing */ -export const generator = FastCheck.tuple(NetworkId.generator, Credential.generator).map( +export const arbitrary = FastCheck.tuple(NetworkId.arbitrary, Credential.arbitrary).map( ([networkId, stakeCredential]) => - new RewardAccount({ + make({ networkId, stakeCredential }) ) -export const Codec = _Codec.createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - RewardAccountError -) +/** + * Effect namespace for RewardAccount operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert bytes to RewardAccount using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromBytes = (bytes: Uint8Array) => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => new RewardAccountError({ message: "Failed to decode from bytes", cause }) + ) + + /** + * Convert hex string to RewardAccount using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromHex = (hex: string) => + Eff.mapError( + Schema.decode(FromHex)(hex), + (cause) => new RewardAccountError({ message: "Failed to decode from hex", cause }) + ) + + /** + * Convert RewardAccount to bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toBytes = (account: RewardAccount) => + Eff.mapError( + Schema.encode(FromBytes)(account), + (cause) => new RewardAccountError({ message: "Failed to encode to bytes", cause }) + ) + + /** + * Convert RewardAccount to hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toHex = (account: RewardAccount) => + Eff.mapError( + Schema.encode(FromHex)(account), + (cause) => new RewardAccountError({ message: "Failed to encode to hex", cause }) + ) +} + +/** + * Convert bytes to RewardAccount (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromBytes = (bytes: Uint8Array): RewardAccount => Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Convert hex string to RewardAccount (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromHex = (hex: string): RewardAccount => Eff.runSync(Effect.fromHex(hex)) + +/** + * Convert RewardAccount to bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toBytes = (account: RewardAccount): Uint8Array => Eff.runSync(Effect.toBytes(account)) + +/** + * Convert RewardAccount to hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toHex = (account: RewardAccount): string => Eff.runSync(Effect.toHex(account)) diff --git a/packages/evolution/src/RewardAddress.ts b/packages/evolution/src/RewardAddress.ts index 6e734c1f..2beafc22 100644 --- a/packages/evolution/src/RewardAddress.ts +++ b/packages/evolution/src/RewardAddress.ts @@ -1,4 +1,15 @@ -import { Schema } from "effect" +import { Data, FastCheck, Schema } from "effect" + +/** + * Error class for RewardAddress related operations. + * + * @since 2.0.0 + * @category errors + */ +export class RewardAddressError extends Data.TaggedError("RewardAddressError")<{ + message?: string + cause?: unknown +}> {} /** * Reward address format schema (human-readable addresses) @@ -7,9 +18,12 @@ import { Schema } from "effect" * @since 2.0.0 * @category schemas */ -export const RewardAddress = Schema.String.pipe(Schema.pattern(/^(stake|stake_test)[1][a-z0-9]+$/i)).pipe( +export const RewardAddress = Schema.String.pipe( + Schema.pattern(/^(stake|stake_test)[1][a-z0-9]+$/i), Schema.brand("RewardAddress") -) +).annotations({ + identifier: "RewardAddress" +}) /** * Type representing a reward/stake address string in bech32 format @@ -17,7 +31,23 @@ export const RewardAddress = Schema.String.pipe(Schema.pattern(/^(stake|stake_te * @since 2.0.0 * @category model */ -export type RewardAddress = Schema.Schema.Type +export type RewardAddress = typeof RewardAddress.Type + +/** + * Smart constructor for RewardAddress that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = RewardAddress.make + +/** + * Check if two RewardAddress instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: RewardAddress, b: RewardAddress): boolean => a === b /** * Check if the given value is a valid RewardAddress @@ -26,3 +56,13 @@ export type RewardAddress = Schema.Schema.Type * @category predicates */ export const isRewardAddress = Schema.is(RewardAddress) + +/** + * FastCheck arbitrary for generating random RewardAddress instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.string({ minLength: 50, maxLength: 100 }).filter((str) => + /^(stake|stake_test)[1][a-z0-9]+$/i.test(str) +).map((str) => make(str)) diff --git a/packages/evolution/src/ScriptDataHash.ts b/packages/evolution/src/ScriptDataHash.ts index 52868224..251c0d24 100644 --- a/packages/evolution/src/ScriptDataHash.ts +++ b/packages/evolution/src/ScriptDataHash.ts @@ -1,7 +1,18 @@ -import { Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" +/** + * Error class for ScriptDataHash related operations. + * + * @since 2.0.0 + * @category errors + */ +export class ScriptDataHashError extends Data.TaggedError("ScriptDataHashError")<{ + message?: string + cause?: unknown +}> {} + /** * ScriptDataHash based on Conway CDDL specification * @@ -15,8 +26,191 @@ import * as Bytes32 from "./Bytes32.js" * (in field 18 of protocol_param_update.) * * @since 2.0.0 - * @category model + * @category schemas */ -export const ScriptDataHash = Bytes32.HexSchema.pipe(Schema.brand("ScriptDataHash")) +export const ScriptDataHash = Bytes32.HexSchema.pipe(Schema.brand("ScriptDataHash")).annotations({ + identifier: "ScriptDataHash" +}) + +export type ScriptDataHash = typeof ScriptDataHash.Type + +/** + * Schema for transforming between Uint8Array and ScriptDataHash. + * + * @since 2.0.0 + * @category schemas + */ +export const FromBytes = Schema.compose( + Bytes32.FromBytes, // Uint8Array -> hex string + ScriptDataHash // hex string -> ScriptDataHash +).annotations({ + identifier: "ScriptDataHash.Bytes" +}) + +/** + * Schema for transforming between hex string and ScriptDataHash. + * + * @since 2.0.0 + * @category schemas + */ +export const FromHex = Schema.compose( + Bytes32.HexSchema, // string -> hex string + ScriptDataHash // hex string -> ScriptDataHash +).annotations({ + identifier: "ScriptDataHash.Hex" +}) + +/** + * Smart constructor for ScriptDataHash that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = ScriptDataHash.make + +/** + * Check if two ScriptDataHash instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: ScriptDataHash, b: ScriptDataHash): boolean => a === b + +/** + * Check if the given value is a valid ScriptDataHash + * + * @since 2.0.0 + * @category predicates + */ +export const isScriptDataHash = Schema.is(ScriptDataHash) + +/** + * FastCheck arbitrary for generating random ScriptDataHash instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes32.HEX_LENGTH, + maxLength: Bytes32.HEX_LENGTH +}).map((hex) => hex as ScriptDataHash) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse ScriptDataHash from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): ScriptDataHash => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse ScriptDataHash from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): ScriptDataHash => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode ScriptDataHash to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (scriptDataHash: ScriptDataHash): Uint8Array => + Eff.runSync(Effect.toBytes(scriptDataHash)) + +/** + * Encode ScriptDataHash to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (scriptDataHash: ScriptDataHash): string => + Eff.runSync(Effect.toHex(scriptDataHash)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse ScriptDataHash from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new ScriptDataHashError({ + message: "Failed to parse ScriptDataHash from bytes", + cause + }) + ) + ) + + /** + * Parse ScriptDataHash from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new ScriptDataHashError({ + message: "Failed to parse ScriptDataHash from hex", + cause + }) + ) + ) + + /** + * Encode ScriptDataHash to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (scriptDataHash: ScriptDataHash): Eff.Effect => + Schema.encode(FromBytes)(scriptDataHash).pipe( + Eff.mapError( + (cause) => + new ScriptDataHashError({ + message: "Failed to encode ScriptDataHash to bytes", + cause + }) + ) + ) -export type ScriptDataHash = Schema.Schema.Type + /** + * Encode ScriptDataHash to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (scriptDataHash: ScriptDataHash): Eff.Effect => + Schema.encode(FromHex)(scriptDataHash).pipe( + Eff.mapError( + (cause) => + new ScriptDataHashError({ + message: "Failed to encode ScriptDataHash to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/ScriptHash.ts b/packages/evolution/src/ScriptHash.ts index 78ca1867..9ba83ad3 100644 --- a/packages/evolution/src/ScriptHash.ts +++ b/packages/evolution/src/ScriptHash.ts @@ -1,6 +1,5 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, pipe, Schema } from "effect" -import { createEncoders } from "./Codec.js" import * as Hash28 from "./Hash28.js" /** @@ -34,7 +33,7 @@ export type ScriptHash = typeof ScriptHash.Type * @since 2.0.0 * @category schemas */ -export const BytesSchema = Schema.compose( +export const FromBytes = Schema.compose( Hash28.FromBytes, // Uint8Array -> hex string ScriptHash // hex string -> ScriptHash ).annotations({ @@ -47,13 +46,21 @@ export const BytesSchema = Schema.compose( * @since 2.0.0 * @category schemas */ -export const HexSchema = Schema.compose( +export const FromHex = Schema.compose( Hash28.HexSchema, // string -> hex string ScriptHash // hex string -> ScriptHash ).annotations({ identifier: "ScriptHash.Hex" }) +/** + * Smart constructor for ScriptHash that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = ScriptHash.make + /** * Check if two ScriptHash instances are equal. * @@ -63,26 +70,111 @@ export const HexSchema = Schema.compose( export const equals = (a: ScriptHash, b: ScriptHash): boolean => a === b /** - * Generate a random ScriptHash. + * FastCheck arbitrary for generating random ScriptHash instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Hash28.HASH28_BYTES_LENGTH, - maxLength: Hash28.HASH28_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.uint8Array({ + minLength: Hash28.BYTES_LENGTH, + maxLength: Hash28.BYTES_LENGTH +}).map((bytes) => Eff.runSync(Effect.fromBytes(bytes))) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for ScriptHash encoding and decoding operations. + * Parse ScriptHash from raw bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: BytesSchema, - hex: HexSchema - }, - ScriptHashError -) +export const fromBytes = (bytes: Uint8Array): ScriptHash => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse ScriptHash from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): ScriptHash => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode ScriptHash to raw bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (scriptHash: ScriptHash): Uint8Array => + Eff.runSync(Effect.toBytes(scriptHash)) + +/** + * Encode ScriptHash to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (scriptHash: ScriptHash): string => scriptHash // Already a hex string + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse ScriptHash from raw bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => + new ScriptHashError({ + message: "Failed to parse ScriptHash from bytes", + cause + }) + ) + + /** + * Parse ScriptHash from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Eff.mapError( + Schema.decode(ScriptHash)(hex), + (cause) => + new ScriptHashError({ + message: "Failed to parse ScriptHash from hex", + cause + }) + ) + + /** + * Encode ScriptHash to raw bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (scriptHash: ScriptHash): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(scriptHash), + (cause) => + new ScriptHashError({ + message: "Failed to encode ScriptHash to bytes", + cause + }) + ) +} diff --git a/packages/evolution/src/ScriptRef.ts b/packages/evolution/src/ScriptRef.ts index b806ce93..b29f233f 100644 --- a/packages/evolution/src/ScriptRef.ts +++ b/packages/evolution/src/ScriptRef.ts @@ -114,7 +114,7 @@ export const FromCDDL = Schema.transformOrFail(CBOR.Tag, ScriptRef, { * @since 2.0.0 * @category schemas */ -export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → ScriptRef @@ -128,7 +128,7 @@ export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) * @since 2.0.0 * @category schemas */ -export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array FromCBORBytes(options) // Uint8Array → ScriptRef @@ -161,7 +161,7 @@ export const generator = FastCheck.uint8Array({ * @since 2.0.0 * @category encoding/decoding */ -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const Codec = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => createEncoders( { bytes: FromBytes, diff --git a/packages/evolution/src/SingleHostAddr.ts b/packages/evolution/src/SingleHostAddr.ts index a900da15..91028b94 100644 --- a/packages/evolution/src/SingleHostAddr.ts +++ b/packages/evolution/src/SingleHostAddr.ts @@ -1,8 +1,7 @@ -import { Data, Effect, FastCheck, Option, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Option, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as IPv4 from "./IPv4.js" import * as IPv6 from "./IPv6.js" import * as Port from "./Port.js" @@ -29,16 +28,7 @@ export class SingleHostAddr extends Schema.TaggedClass()("Single port: Schema.OptionFromNullOr(Port.PortSchema), ipv4: Schema.OptionFromNullOr(IPv4.IPv4), ipv6: Schema.OptionFromNullOr(IPv6.IPv6) -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: "SingleHostAddr", - port: this.port, - ipv4: this.ipv4, - ipv6: this.ipv6 - } - } -} +}) {} /** * Create a SingleHostAddr with IPv4 address. @@ -114,25 +104,6 @@ export const equals = (a: SingleHostAddr, b: SingleHostAddr): boolean => Option.getEquivalence(IPv4.equals)(a.ipv4, b.ipv4) && Option.getEquivalence(IPv6.equals)(a.ipv6, b.ipv6) -/** - * Generate a random SingleHostAddr. - * - * @since 2.0.0 - * @category generators - */ -export const generator = FastCheck.record({ - port: FastCheck.option(Port.generator), - ipv4: FastCheck.option(IPv4.generator), - ipv6: FastCheck.option(IPv6.generator) -}).map( - ({ ipv4, ipv6, port }) => - new SingleHostAddr({ - port: port ? Option.some(port) : Option.none(), - ipv4: ipv4 ? Option.some(ipv4) : Option.none(), - ipv6: ipv6 ? Option.some(ipv6) : Option.none() - }) -) - /** * CDDL schema for SingleHostAddr. * single_host_addr = (0, port / nil, ipv4 / nil, ipv6 / nil) @@ -140,7 +111,7 @@ export const generator = FastCheck.record({ * @since 2.0.0 * @category schemas */ -export const SingleHostAddrCDDLSchema = Schema.transformOrFail( +export const FromCDDL = Schema.transformOrFail( Schema.Tuple( Schema.Literal(0n), // tag (literal 0) Schema.NullOr(CBOR.Integer), // port (number or null) @@ -151,17 +122,17 @@ export const SingleHostAddrCDDLSchema = Schema.transformOrFail( { strict: true, encode: (toA) => - Effect.gen(function* () { + Eff.gen(function* () { const port = Option.isSome(toA.port) ? BigInt(toA.port.value) : null const ipv4 = Option.isSome(toA.ipv4) ? yield* ParseResult.encode(IPv4.FromBytes)(toA.ipv4.value) : null const ipv6 = Option.isSome(toA.ipv6) ? yield* ParseResult.encode(IPv6.FromBytes)(toA.ipv6.value) : null - return yield* Effect.succeed([0n, port, ipv4, ipv6] as const) + return yield* Eff.succeed([0n, port, ipv4, ipv6] as const) }), decode: ([, portValue, ipv4Value, ipv6Value]) => - Effect.gen(function* () { + Eff.gen(function* () { const port = portValue === null || portValue === undefined ? Option.none() : Option.some(Port.make(Number(portValue))) @@ -175,9 +146,42 @@ export const SingleHostAddrCDDLSchema = Schema.transformOrFail( ? Option.none() : Option.some(yield* ParseResult.decode(IPv6.FromBytes)(ipv6Value)) - return yield* Effect.succeed(new SingleHostAddr({ port, ipv4, ipv6 })) + return yield* Eff.succeed(new SingleHostAddr({ port, ipv4, ipv6 })) }) } +).annotations({ + identifier: "SingleHostAddr.SingleHostAddrCDDLSchema", + description: "Transforms CBOR structure to SingleHostAddr" +}) + +/** + * Smart constructor for creating SingleHostAddr instances + * + * @since 2.0.0 + * @category constructors + */ +export const make = (props: { + port: Option.Option + ipv4: Option.Option + ipv6: Option.Option +}): SingleHostAddr => new SingleHostAddr(props) + +/** + * FastCheck arbitrary for generating random SingleHostAddr instances + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary = FastCheck.record({ + port: FastCheck.option(Port.arbitrary), + ipv4: FastCheck.option(IPv4.arbitrary), + ipv6: FastCheck.option(IPv6.arbitrary) +}).map(({ ipv4, ipv6, port }) => + make({ + port: port ? Option.some(port) : Option.none(), + ipv4: ipv4 ? Option.some(ipv4) : Option.none(), + ipv6: ipv6 ? Option.some(ipv6) : Option.none() + }) ) /** @@ -186,11 +190,14 @@ export const SingleHostAddrCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - SingleHostAddrCDDLSchema // CBOR → SingleHostAddr - ) + FromCDDL // CBOR → SingleHostAddr + ).annotations({ + identifier: "SingleHostAddr.FromCBORBytes", + description: "Transforms CBOR bytes to SingleHostAddr" + }) /** * CBOR hex transformation schema for SingleHostAddr. @@ -198,17 +205,103 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → SingleHostAddr - ) - -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - SingleHostAddrError - ) + FromCBORBytes(options) // Uint8Array → SingleHostAddr + ).annotations({ + identifier: "SingleHostAddr.FromCBORHex", + description: "Transforms CBOR hex string to SingleHostAddr" + }) + +/** + * Effect namespace for SingleHostAddr operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert CBOR bytes to SingleHostAddr using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (cause) => new SingleHostAddrError({ message: "Failed to decode from CBOR bytes", cause }) + ) + + /** + * Convert CBOR hex string to SingleHostAddr using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (cause) => new SingleHostAddrError({ message: "Failed to decode from CBOR hex", cause }) + ) + + /** + * Convert SingleHostAddr to CBOR bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORBytes = (hostAddr: SingleHostAddr, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(hostAddr), + (cause) => new SingleHostAddrError({ message: "Failed to encode to CBOR bytes", cause }) + ) + + /** + * Convert SingleHostAddr to CBOR hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORHex = (hostAddr: SingleHostAddr, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(hostAddr), + (cause) => new SingleHostAddrError({ message: "Failed to encode to CBOR hex", cause }) + ) +} + +/** + * Convert CBOR bytes to SingleHostAddr (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): SingleHostAddr => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Convert CBOR hex string to SingleHostAddr (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): SingleHostAddr => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Convert SingleHostAddr to CBOR bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORBytes = (hostAddr: SingleHostAddr, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(hostAddr, options)) + +/** + * Convert SingleHostAddr to CBOR hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORHex = (hostAddr: SingleHostAddr, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(hostAddr, options)) diff --git a/packages/evolution/src/SingleHostName.ts b/packages/evolution/src/SingleHostName.ts index 273035df..2f651e50 100644 --- a/packages/evolution/src/SingleHostName.ts +++ b/packages/evolution/src/SingleHostName.ts @@ -102,8 +102,8 @@ export const equals = (a: SingleHostName, b: SingleHostName): boolean => * @category generators */ export const generator = FastCheck.record({ - port: FastCheck.option(Port.generator), - dnsName: DnsName.generator + port: FastCheck.option(Port.arbitrary), + dnsName: DnsName.arbitrary }).map( ({ dnsName, port }) => new SingleHostName({ @@ -119,7 +119,7 @@ export const generator = FastCheck.record({ * @since 2.0.0 * @category schemas */ -export const SingleHostNameCDDLSchema = Schema.transformOrFail( +export const FromCDDL = Schema.transformOrFail( Schema.Tuple( Schema.Literal(1n), // tag (literal 1) Schema.NullOr(CBOR.Integer), // port (number or null) @@ -155,10 +155,10 @@ export const SingleHostNameCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - SingleHostNameCDDLSchema // CBOR → SingleHostName + FromCDDL // CBOR → SingleHostName ) /** @@ -167,13 +167,13 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array FromBytes(options) // Uint8Array → SingleHostName ) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => ({ +export const Codec = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => ({ Encode: { cborBytes: Schema.encodeSync(FromBytes(options)), cborHex: Schema.encodeSync(FromHex(options)) diff --git a/packages/evolution/src/TSchema.ts b/packages/evolution/src/TSchema.ts index 041346c0..26bc41d6 100644 --- a/packages/evolution/src/TSchema.ts +++ b/packages/evolution/src/TSchema.ts @@ -20,7 +20,7 @@ interface ByteArray extends Schema.refine {} * * @since 2.0.0 */ -export const ByteArray: ByteArray = Data.BytesSchema.annotations({ +export const ByteArray: ByteArray = Data.ByteArray.annotations({ identifier: "TSchema.ByteArray" }) // : Schema.Schema = Schema.transform( diff --git a/packages/evolution/src/Text.ts b/packages/evolution/src/Text.ts index e2bd3c40..f6868d0c 100644 --- a/packages/evolution/src/Text.ts +++ b/packages/evolution/src/Text.ts @@ -1,7 +1,6 @@ -import { Data, Schema } from "effect" +import { Data, Either as E, FastCheck, Schema } from "effect" import * as Bytes from "./Bytes.js" -import { createEncoders } from "./Codec.js" /** * Error class for Text related operations. @@ -26,7 +25,8 @@ export const FromBytes = Schema.transform(Schema.Uint8ArrayFromSelf, Schema.Stri encode: (fromA) => new TextEncoder().encode(fromA), decode: (toA) => new TextDecoder().decode(toA) }).annotations({ - identifier: "Text.FromBytes" + identifier: "Text.FromBytes", + description: "Transforms UTF-8 bytes to string" }) /** @@ -40,19 +40,126 @@ export const FromBytes = Schema.transform(Schema.Uint8ArrayFromSelf, Schema.Stri * @category schemas */ export const FromHex = Schema.compose(Bytes.FromHex, FromBytes).annotations({ - identifier: "Text.FromHex" + identifier: "Text.FromHex", + description: "Transforms hex string to UTF-8 text" }) /** - * Codec utilities for Text encoding and decoding operations. + * FastCheck arbitrary for generating random text strings * * @since 2.0.0 - * @category encoding/decoding + * @category testing */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - TextError -) +export const arbitrary = FastCheck.string() + +/** + * Either namespace for Text operations that can fail + * + * @since 2.0.0 + * @category either + */ +export namespace Either { + /** + * Convert bytes to text using Either + * + * @since 2.0.0 + * @category conversion + */ + export const fromBytes = (bytes: Uint8Array) => + E.mapLeft( + Schema.decodeEither(FromBytes)(bytes), + (cause) => new TextError({ message: "Failed to decode from bytes", cause }) + ) + + /** + * Convert hex string to text using Either + * + * @since 2.0.0 + * @category conversion + */ + export const fromHex = (hex: string) => + E.mapLeft( + Schema.decodeEither(FromHex)(hex), + (cause) => new TextError({ message: "Failed to decode from hex", cause }) + ) + + /** + * Convert text to bytes using Either + * + * @since 2.0.0 + * @category conversion + */ + export const toBytes = (text: string) => + E.mapLeft( + Schema.encodeEither(FromBytes)(text), + (cause) => new TextError({ message: "Failed to encode to bytes", cause }) + ) + + /** + * Convert text to hex string using Either + * + * @since 2.0.0 + * @category conversion + */ + export const toHex = (text: string) => + E.mapLeft( + Schema.encodeEither(FromHex)(text), + (cause) => new TextError({ message: "Failed to encode to hex", cause }) + ) +} + +/** + * Convert bytes to text (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromBytes = (bytes: Uint8Array): string => { + try { + return Schema.decodeSync(FromBytes)(bytes) + } catch (cause) { + throw new TextError({ message: "Failed to decode from bytes", cause }) + } +} + +/** + * Convert hex string to text (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromHex = (hex: string): string => { + try { + return Schema.decodeSync(FromHex)(hex) + } catch (cause) { + throw new TextError({ message: "Failed to decode from hex", cause }) + } +} + +/** + * Convert text to bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toBytes = (text: string): Uint8Array => { + try { + return Schema.encodeSync(FromBytes)(text) + } catch (cause) { + throw new TextError({ message: "Failed to encode to bytes", cause }) + } +} + +/** + * Convert text to hex string (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toHex = (text: string): string => { + try { + return Schema.encodeSync(FromHex)(text) + } catch (cause) { + throw new TextError({ message: "Failed to encode to hex", cause }) + } +} diff --git a/packages/evolution/src/Text128.ts b/packages/evolution/src/Text128.ts index 73bdb13b..692acb4b 100644 --- a/packages/evolution/src/Text128.ts +++ b/packages/evolution/src/Text128.ts @@ -1,6 +1,5 @@ -import { Data, FastCheck, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" -import { createEncoders } from "./Codec.js" import * as Text from "./Text.js" /** @@ -47,15 +46,177 @@ export const FromVariableHex = Text.FromHex.pipe( identifier: "Text128.FromHex" }) -export const generator = FastCheck.string({ +/** + * Schema for Text128 representing a variable-length text string (0-128 chars). + * text .size (0 .. 128) + * Follows the Conway-era CDDL specification. + * + * @since 2.0.0 + * @category schemas + */ +export const Text128 = FromVariableHex.pipe(Schema.brand("Text128")).annotations({ + identifier: "Text128" +}) + +export type Text128 = typeof Text128.Type + +export const FromBytes = Schema.compose( + FromVariableBytes, // Uint8Array -> string + Text128 // string -> Text128 +).annotations({ + identifier: "Text128.Bytes" +}) + +export const FromHex = Schema.compose( + FromVariableHex, // string -> string + Text128 // string -> Text128 +).annotations({ + identifier: "Text128.Hex" +}) + +/** + * Check if two Text128 instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: Text128, b: Text128): boolean => a === b + +/** + * Check if the given value is a valid Text128 + * + * @since 2.0.0 + * @category predicates + */ +export const isText128 = Schema.is(Text128) + +/** + * FastCheck arbitrary for generating random Text128 instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.string({ minLength: TEXT128_MIN_LENGTH, maxLength: TEXT128_MAX_LENGTH -}) +}).map((text) => text as Text128) -export const Codec = createEncoders( - { - bytes: FromVariableBytes, - hex: FromVariableHex - }, - Text128Error -) +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse Text128 from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): Text128 => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse Text128 from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): Text128 => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode Text128 to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (text: Text128): Uint8Array => + Eff.runSync(Effect.toBytes(text)) + +/** + * Encode Text128 to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (text: Text128): string => + Eff.runSync(Effect.toHex(text)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Text128 from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new Text128Error({ + message: "Failed to parse Text128 from bytes", + cause + }) + ) + ) + + /** + * Parse Text128 from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new Text128Error({ + message: "Failed to parse Text128 from hex", + cause + }) + ) + ) + + /** + * Encode Text128 to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (text: Text128): Eff.Effect => + Schema.encode(FromBytes)(text).pipe( + Eff.mapError( + (cause) => + new Text128Error({ + message: "Failed to encode Text128 to bytes", + cause + }) + ) + ) + + /** + * Encode Text128 to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (text: Text128): Eff.Effect => + Schema.encode(FromHex)(text).pipe( + Eff.mapError( + (cause) => + new Text128Error({ + message: "Failed to encode Text128 to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/TransactionBody.ts b/packages/evolution/src/TransactionBody.ts index 3b565352..30787e50 100644 --- a/packages/evolution/src/TransactionBody.ts +++ b/packages/evolution/src/TransactionBody.ts @@ -1,13 +1,17 @@ -import { Schema } from "effect" +import { Data, Effect as Eff, ParseResult, Schema } from "effect" +import type { NonEmptyArray } from "effect/Array" import * as AuxiliaryDataHash from "./AuxiliaryDataHash.js" +import * as CBOR from "./CBOR.js" import * as Certificate from "./Certificate.js" import * as Coin from "./Coin.js" import * as Hash28 from "./Hash28.js" +import * as KeyHash from "./KeyHash.js" import * as Mint from "./Mint.js" import * as NetworkId from "./NetworkId.js" import * as PositiveCoin from "./PositiveCoin.js" import * as ProposalProcedures from "./ProposalProcedures.js" +import * as RewardAccount from "./RewardAccount.js" import * as ScriptDataHash from "./ScriptDataHash.js" import * as TransactionInput from "./TransactionInput.js" import * as TransactionOutput from "./TransactionOutput.js" @@ -48,7 +52,7 @@ import * as Withdrawals from "./Withdrawals.js" export class TransactionBody extends Schema.TaggedClass()("TransactionBody", { inputs: Schema.NonEmptyArray(TransactionInput.TransactionInput), // 0 outputs: Schema.Array(TransactionOutput.TransactionOutput), // 1 - fee: Coin.CoinSchema, // 2 + fee: Coin.Coin, // 2 ttl: Schema.optional(Schema.BigIntFromSelf), // 3 - slot_no certificates: Schema.optional(Schema.NonEmptyArray(Certificate.Certificate)), // 4 withdrawals: Schema.optional(Withdrawals.Withdrawals), // 5 @@ -57,15 +61,380 @@ export class TransactionBody extends Schema.TaggedClass()("Tran mint: Schema.optional(Mint.Mint), // 9 scriptDataHash: Schema.optional(ScriptDataHash.ScriptDataHash), // 11 collateralInputs: Schema.optional(Schema.NonEmptyArray(TransactionInput.TransactionInput)), // 13 - requiredSigners: Schema.optional(Schema.NonEmptyArray(Hash28.HexSchema)), // 14 + requiredSigners: Schema.optional(Schema.NonEmptyArray(KeyHash.KeyHash)), // 14 networkId: Schema.optional(NetworkId.NetworkId), // 15 collateralReturn: Schema.optional(TransactionOutput.TransactionOutput), // 16 - totalCollateral: Schema.optional(Coin.CoinSchema), // 17 + totalCollateral: Schema.optional(Coin.Coin), // 17 referenceInputs: Schema.optional(Schema.NonEmptyArray(TransactionInput.TransactionInput)), // 18 votingProcedures: Schema.optional(VotingProcedures.VotingProcedures), // 19 proposalProcedures: Schema.optional(ProposalProcedures.ProposalProcedures), // 20 - currentTreasuryValue: Schema.optional(Coin.CoinSchema), // 21 + currentTreasuryValue: Schema.optional(Coin.Coin), // 21 donation: Schema.optional(PositiveCoin.PositiveCoinSchema) // 22 }) {} -//TODO: Implement FromHex when BytesSchema is complete +/** + * Error class for TransactionBody related operations. + * + * @since 2.0.0 + * @category errors + */ +export class TransactionBodyError extends Data.TaggedError("TransactionBodyError")<{ + message?: string + cause?: unknown +}> {} + +/** + * CDDL schema for TransactionBody map structure. + * + * Maps the TransactionBody fields to their CDDL field numbers according to Conway spec. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = Schema.Struct({ + 0: Schema.Array(Schema.encodedSchema(TransactionInput.CDDLSchema)), // set + 1: Schema.Array(Schema.encodedSchema(TransactionOutput.CDDLSchema)), // [* transaction_output] + 2: CBOR.Integer, // coin + 3: Schema.optional(CBOR.Integer), // slot_no (ttl) + 4: Schema.optional(Schema.Array(Schema.encodedSchema(Certificate.CDDLSchema))), // certificates + 5: Schema.optional(Withdrawals.CDDLSchema), // withdrawals + 7: Schema.optional(CBOR.ByteArray), // auxiliary_data_hash + 8: Schema.optional(CBOR.Integer), // slot_no (validity_interval_start) + 9: Schema.optional(Schema.encodedSchema(Mint.CDDLSchema)), // mint + 11: Schema.optional(CBOR.ByteArray), // script_data_hash + 13: Schema.optional(Schema.Array(TransactionInput.CDDLSchema)), // nonempty_set + 14: Schema.optional(Schema.Array(CBOR.ByteArray)), // required_signers + 15: Schema.optional(CBOR.Integer), // network_id + 16: Schema.optional(Schema.encodedSchema(TransactionOutput.CDDLSchema)), // transaction_output + 17: Schema.optional(CBOR.Integer), // coin + 18: Schema.optional(Schema.Array(Schema.encodedSchema(TransactionInput.CDDLSchema))), // nonempty_set + 19: Schema.optional(Schema.encodedSchema(VotingProcedures.CDDLSchema)), // voting_procedures + 20: Schema.optional(Schema.encodedSchema(ProposalProcedures.CDDLSchema)), // proposal_procedures + 21: Schema.optional(CBOR.Integer), // coin + 22: Schema.optional(CBOR.Integer) // positive_coin +}) + +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(TransactionBody), { + strict: true, + encode: (toA) => + Eff.gen(function* () { + // Required fields + const inputs = yield* Eff.all(toA.inputs.map((input) => ParseResult.encode(TransactionInput.FromCDDL)(input))) + const outputs = yield* Eff.all( + toA.outputs.map((output) => ParseResult.encode(TransactionOutput.FromTransactionOutputCDDLSchema)(output)) + ) + const fee = toA.fee + + // Optional fields + const ttl = toA.ttl + const certificates = toA.certificates + ? yield* Eff.all(toA.certificates.map((cert) => ParseResult.encode(Certificate.FromCDDL)(cert))) + : undefined + const withdrawalsMap = new Map() + if (toA.withdrawals) { + for (const [rewardAccount, coin] of toA.withdrawals.withdrawals.entries()) { + const accountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(rewardAccount) + withdrawalsMap.set(accountBytes, coin) + } + } + const withdrawals = toA.withdrawals ? withdrawalsMap : undefined + const auxiliaryDataHash = toA.auxiliaryDataHash + ? yield* ParseResult.encode(AuxiliaryDataHash.BytesSchema)(toA.auxiliaryDataHash) + : undefined + const validityIntervalStart = toA.validityIntervalStart + const mint = toA.mint ? yield* ParseResult.encode(Mint.MintCDDLSchema)(toA.mint) : undefined + const scriptDataHash = toA.scriptDataHash + ? yield* ParseResult.encode(ScriptDataHash.FromBytes)(toA.scriptDataHash) + : undefined + const collateralInputs = toA.collateralInputs + ? yield* Eff.all(toA.collateralInputs.map((input) => ParseResult.encode(TransactionInput.FromCDDL)(input))) + : undefined + const requiredSigners = toA.requiredSigners + ? yield* Eff.all(toA.requiredSigners.map((signer) => ParseResult.encode(Hash28.FromBytes)(signer))) + : undefined + const networkId = toA.networkId ? BigInt(toA.networkId) : undefined + const collateralReturn = toA.collateralReturn + ? yield* ParseResult.encode(TransactionOutput.FromTransactionOutputCDDLSchema)(toA.collateralReturn) + : undefined + const totalCollateral = toA.totalCollateral + const referenceInputs = toA.referenceInputs + ? yield* Eff.all(toA.referenceInputs.map((input) => ParseResult.encode(TransactionInput.FromCDDL)(input))) + : undefined + const votingProcedures = toA.votingProcedures + ? yield* ParseResult.encode(VotingProcedures.FromCDDL)(toA.votingProcedures) + : undefined + const proposalProcedures = toA.proposalProcedures + ? yield* ParseResult.encode(ProposalProcedures.FromCDDL)(toA.proposalProcedures) + : undefined + const currentTreasuryValue = toA.currentTreasuryValue + const donation = toA.donation + + return { + 0: inputs, + 1: outputs, + 2: fee, + 3: ttl, + 4: certificates, + 5: withdrawals, + 7: auxiliaryDataHash, + 8: validityIntervalStart, + 9: mint, + 11: scriptDataHash, + 13: collateralInputs, + 14: requiredSigners, + 15: networkId, + 16: collateralReturn, + 17: totalCollateral, + 18: referenceInputs, + 19: votingProcedures, + 20: proposalProcedures, + 21: currentTreasuryValue, + 22: donation + } + }), + decode: (fromA) => + Eff.gen(function* () { + // Required fields + const inputs = (yield* Eff.all( + fromA[0].map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) + )) as NonEmptyArray + + const outputs = yield* Eff.all( + fromA[1].map((output) => ParseResult.decode(TransactionOutput.FromTransactionOutputCDDLSchema)(output)) + ) + const fee = fromA[2] + + // Optional fields + const ttl = fromA[3] + + const certificates = fromA[4] + ? ((yield* Eff.all( + fromA[4].map((cert) => ParseResult.decode(Certificate.FromCDDL)(cert)) + )) as NonEmptyArray) + : undefined + + let withdrawals: Withdrawals.Withdrawals | undefined + if (fromA[5]) { + const decodedWithdrawals = new Map() + for (const [accountBytes, coinAmount] of fromA[5].entries()) { + const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(accountBytes) + decodedWithdrawals.set(rewardAccount, coinAmount) + } + withdrawals = new Withdrawals.Withdrawals({ withdrawals: decodedWithdrawals }) + } + + const auxiliaryDataHash = fromA[7] + ? yield* ParseResult.decode(AuxiliaryDataHash.BytesSchema)(fromA[7]) + : undefined + const validityIntervalStart = fromA[8] + const mint = fromA[9] ? yield* ParseResult.decode(Mint.MintCDDLSchema)(fromA[9]) : undefined + const scriptDataHash = fromA[11] ? yield* ParseResult.decode(ScriptDataHash.FromBytes)(fromA[11]) : undefined + + const collateralInputs = fromA[13] + ? ((yield* Eff.all( + fromA[13].map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) + )) as NonEmptyArray) + : undefined + + const requiredSigners = fromA[14] + ? ((yield* Eff.all( + fromA[14].map((signer) => ParseResult.decode(KeyHash.FromBytes)(signer)) + )) as NonEmptyArray) + : undefined + + const networkId = fromA[15] ? NetworkId.make(Number(fromA[15])) : undefined + const collateralReturn = fromA[16] + ? yield* ParseResult.decode(TransactionOutput.FromTransactionOutputCDDLSchema)(fromA[16]) + : undefined + const totalCollateral = fromA[17] + + const referenceInputs = fromA[18] + ? ((yield* Eff.all( + fromA[18].map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) + )) as NonEmptyArray) + : undefined + const votingProcedures = fromA[19] ? yield* ParseResult.decode(VotingProcedures.FromCDDL)(fromA[19]) : undefined + const proposalProcedures = fromA[20] + ? yield* ParseResult.decode(ProposalProcedures.FromCDDL)(fromA[20]) + : undefined + const currentTreasuryValue = fromA[21] + const donation = fromA[22] + + return new TransactionBody({ + inputs, + outputs, + fee, + ttl, + certificates, + withdrawals, + auxiliaryDataHash, + validityIntervalStart, + mint, + scriptDataHash, + collateralInputs, + requiredSigners, + networkId, + collateralReturn, + totalCollateral, + referenceInputs, + votingProcedures, + proposalProcedures, + currentTreasuryValue, + donation + }) + }) +}) + +/** + * CBOR bytes transformation schema for TransactionBody. + * Transforms between CBOR bytes and TransactionBody using Conway CDDL specification. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose( + CBOR.FromBytes(options), + FromCDDL + ).annotations({ + identifier: "TransactionBody.FromCBORBytes", + title: "TransactionBody from CBOR bytes", + description: "Decode TransactionBody from CBOR-encoded bytes using Conway CDDL specification" + }) + +/** + * CBOR hex transformation schema for TransactionBody. + * Transforms between CBOR hex string and TransactionBody using Conway CDDL specification. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose( + CBOR.FromHex(options), + FromCDDL + ).annotations({ + identifier: "TransactionBody.FromCBORHex", + title: "TransactionBody from CBOR hex", + description: "Decode TransactionBody from CBOR-encoded hex string using Conway CDDL specification" + }) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Decode a TransactionBody from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): TransactionBody => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Decode a TransactionBody from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): TransactionBody => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Encode a TransactionBody to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (transactionBody: TransactionBody, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Uint8Array => + Eff.runSync(Effect.toCBORBytes(transactionBody, options)) + +/** + * Encode a TransactionBody to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (transactionBody: TransactionBody, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): string => + Eff.runSync(Effect.toCBORHex(transactionBody, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Decode a TransactionBody from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new TransactionBodyError({ + message: "Failed to decode TransactionBody from CBOR bytes", + cause + }) + ) + ) + + /** + * Decode a TransactionBody from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new TransactionBodyError({ + message: "Failed to decode TransactionBody from CBOR hex", + cause + }) + ) + ) + + /** + * Encode a TransactionBody to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (transactionBody: TransactionBody, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + Schema.encode(FromCBORBytes(options))(transactionBody).pipe( + Eff.mapError( + (cause) => + new TransactionBodyError({ + message: "Failed to encode TransactionBody to CBOR bytes", + cause + }) + ) + ) + + /** + * Encode a TransactionBody to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (transactionBody: TransactionBody, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + Schema.encode(FromCBORHex(options))(transactionBody).pipe( + Eff.mapError( + (cause) => + new TransactionBodyError({ + message: "Failed to encode TransactionBody to CBOR hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/TransactionHash.ts b/packages/evolution/src/TransactionHash.ts index 5ff46329..ea097515 100644 --- a/packages/evolution/src/TransactionHash.ts +++ b/packages/evolution/src/TransactionHash.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" /** * Error class for TransactionHash related operations. @@ -21,7 +20,7 @@ export class TransactionHashError extends Data.TaggedError("TransactionHashError * @since 2.0.0 * @category schemas */ -export const TransactionHash = pipe(Bytes32.HexSchema, Schema.brand("TransactionHash")).annotations({ +export const TransactionHash = Bytes32.HexSchema.pipe(Schema.brand("TransactionHash")).annotations({ identifier: "TransactionHash" }) @@ -33,7 +32,7 @@ export type TransactionHash = typeof TransactionHash.Type * @since 2.0.0 * @category schemas */ -export const BytesSchema = Schema.compose( +export const FromBytes = Schema.compose( Bytes32.FromBytes, // Uint8Array -> hex string TransactionHash // hex string -> TransactionHash ).annotations({ @@ -46,13 +45,21 @@ export const BytesSchema = Schema.compose( * @since 2.0.0 * @category schemas */ -export const HexSchema = Schema.compose( +export const FromHex = Schema.compose( Bytes32.HexSchema, // string -> hex string TransactionHash // hex string -> TransactionHash ).annotations({ identifier: "TransactionHash.Hex" }) +/** + * Smart constructor for TransactionHash that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = TransactionHash.make + /** * Check if two TransactionHash instances are equal. * @@ -62,26 +69,140 @@ export const HexSchema = Schema.compose( export const equals = (a: TransactionHash, b: TransactionHash): boolean => a === b /** - * Generate a random TransactionHash. + * Check if the given value is a valid TransactionHash + * + * @since 2.0.0 + * @category predicates + */ +export const isTransactionHash = Schema.is(TransactionHash) + +/** + * FastCheck arbitrary for generating random TransactionHash instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes32.Bytes32_BYTES_LENGTH, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes32.HEX_LENGTH, + maxLength: Bytes32.HEX_LENGTH +}).map((hex) => hex as TransactionHash) + +// ============================================================================ +// Root Functions +// ============================================================================ /** - * Codec utilities for TransactionHash encoding and decoding operations. + * Parse TransactionHash from bytes. * * @since 2.0.0 - * @category encoding/decoding + * @category parsing */ -export const Codec = createEncoders( - { - bytes: BytesSchema, - hex: HexSchema - }, - TransactionHashError -) +export const fromBytes = (bytes: Uint8Array): TransactionHash => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse TransactionHash from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): TransactionHash => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode TransactionHash to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (txHash: TransactionHash): Uint8Array => + Eff.runSync(Effect.toBytes(txHash)) + +/** + * Encode TransactionHash to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (txHash: TransactionHash): string => + Eff.runSync(Effect.toHex(txHash)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse TransactionHash from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new TransactionHashError({ + message: "Failed to parse TransactionHash from bytes", + cause + }) + ) + ) + + /** + * Parse TransactionHash from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new TransactionHashError({ + message: "Failed to parse TransactionHash from hex", + cause + }) + ) + ) + + /** + * Encode TransactionHash to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (txHash: TransactionHash): Eff.Effect => + Schema.encode(FromBytes)(txHash).pipe( + Eff.mapError( + (cause) => + new TransactionHashError({ + message: "Failed to encode TransactionHash to bytes", + cause + }) + ) + ) + + /** + * Encode TransactionHash to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (txHash: TransactionHash): Eff.Effect => + Schema.encode(FromHex)(txHash).pipe( + Eff.mapError( + (cause) => + new TransactionHashError({ + message: "Failed to encode TransactionHash to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/TransactionIndex.ts b/packages/evolution/src/TransactionIndex.ts index f31178e5..e20ccc68 100644 --- a/packages/evolution/src/TransactionIndex.ts +++ b/packages/evolution/src/TransactionIndex.ts @@ -1,15 +1,59 @@ -import type { Schema } from "effect" +import { Data, FastCheck, Schema } from "effect" import * as Numeric from "./Numeric.js" /** + * Error class for TransactionIndex related operations. + * + * @since 2.0.0 + * @category errors + */ +export class TransactionIndexError extends Data.TaggedError("TransactionIndexError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Schema for TransactionIndex representing a transaction index within a block. * CDDL: transaction_index = uint .size 2 * * @since 2.0.0 - * @category model + * @category schemas */ -export const TransactionIndexSchema = Numeric.Uint16Schema.annotations({ +export const TransactionIndex = Numeric.Uint16Schema.pipe(Schema.brand("TransactionIndex")).annotations({ identifier: "TransactionIndex" }) -export type TransactionIndex = Schema.Schema.Type +export type TransactionIndex = typeof TransactionIndex.Type + +/** + * Smart constructor for TransactionIndex that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = TransactionIndex.make + +/** + * Check if two TransactionIndex instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: TransactionIndex, b: TransactionIndex): boolean => a === b + +/** + * Check if a value is a valid TransactionIndex. + * + * @since 2.0.0 + * @category predicates + */ +export const is = Schema.is(TransactionIndex) + +/** + * FastCheck arbitrary for generating random TransactionIndex instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.integer({ min: 0, max: 65535 }).map((value) => make(value)) diff --git a/packages/evolution/src/TransactionInput.ts b/packages/evolution/src/TransactionInput.ts index c26ce1cc..fd4185e2 100644 --- a/packages/evolution/src/TransactionInput.ts +++ b/packages/evolution/src/TransactionInput.ts @@ -1,8 +1,7 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as Numeric from "./Numeric.js" import * as TransactionHash from "./TransactionHash.js" @@ -42,6 +41,11 @@ export class TransactionInput extends Schema.TaggedClass()("Tr */ export const isTransactionInput = Schema.is(TransactionInput) +export const CDDLSchema = Schema.Tuple( + Schema.Uint8ArrayFromSelf, // transaction_id as bytes + CBOR.Integer // index as bigint +) + /** * CDDL schema for TransactionInput. * transaction_input = [transaction_id : $Bytes32, index : uint .size 2] @@ -49,30 +53,26 @@ export const isTransactionInput = Schema.is(TransactionInput) * @since 2.0.0 * @category schemas */ -export const TransactionInputCDDLSchema = Schema.transformOrFail( - Schema.Tuple( - Schema.Uint8ArrayFromSelf, // transaction_id as bytes - CBOR.Integer // index as bigint - ), - Schema.typeSchema(TransactionInput), - { - strict: true, - encode: (toA) => - Effect.gen(function* () { - const txHashBytes = yield* ParseResult.encode(TransactionHash.BytesSchema)(toA.transactionId) - return [txHashBytes, BigInt(toA.index)] as const - }), - decode: ([txHashBytes, indexBigInt]) => - Effect.gen(function* () { - const transactionId = yield* ParseResult.decode(TransactionHash.BytesSchema)(txHashBytes) - return yield* ParseResult.decode(TransactionInput)({ - _tag: "TransactionInput", - transactionId, - index: Number(indexBigInt) - }) +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(TransactionInput), { + strict: true, + encode: (toA) => + Eff.gen(function* () { + const txHashBytes = yield* ParseResult.encode(TransactionHash.FromBytes)(toA.transactionId) + return [txHashBytes, BigInt(toA.index)] as const + }), + decode: ([txHashBytes, indexBigInt]) => + Eff.gen(function* () { + const transactionId = yield* ParseResult.decode(TransactionHash.FromBytes)(txHashBytes) + return yield* ParseResult.decode(TransactionInput)({ + _tag: "TransactionInput", + transactionId, + index: Number(indexBigInt) }) - } -) + }) +}).annotations({ + identifier: "TransactionInput.FromCDDL", + description: "Transforms CBOR structure to TransactionInput" +}) /** * CBOR bytes transformation schema for TransactionInput. @@ -80,11 +80,14 @@ export const TransactionInputCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - TransactionInputCDDLSchema // CBOR → TransactionInput - ) + FromCDDL // CBOR → TransactionInput + ).annotations({ + identifier: "TransactionInput.FromCBORBytes", + description: "Transforms CBOR bytes to TransactionInput" + }) /** * CBOR hex transformation schema for TransactionInput. @@ -92,11 +95,26 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → TransactionInput - ) + FromCBORBytes(options) // Uint8Array → TransactionInput + ).annotations({ + identifier: "TransactionInput.FromCBORHex", + description: "Transforms CBOR hex string to TransactionInput" + }) + +/** + * Smart constructor for creating TransactionInput instances + * + * @since 2.0.0 + * @category constructors + */ +export const make = (props: { transactionId: TransactionHash.TransactionHash; index: number }): TransactionInput => + new TransactionInput({ + transactionId: props.transactionId, + index: Numeric.Uint16Make(props.index) + }) /** * Check if two TransactionInput instances are equal. @@ -108,30 +126,107 @@ export const equals = (a: TransactionInput, b: TransactionInput): boolean => a._tag === b._tag && a.index === b.index && a.transactionId === b.transactionId /** - * FastCheck generator for TransactionInput instances. + * FastCheck arbitrary for TransactionInput instances. * * @since 2.0.0 - * @category generators + * @category testing */ -export const generator = FastCheck.tuple(TransactionHash.generator, Numeric.Uint16Generator).map( +export const arbitrary = FastCheck.tuple(TransactionHash.arbitrary, Numeric.Uint16Generator).map( ([transactionId, index]) => - new TransactionInput({ + make({ transactionId, index }) ) /** - * Extended Codec with CBOR support for TransactionInput. + * Effect namespace for TransactionInput operations that can fail + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Convert CBOR bytes to TransactionInput using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(bytes), + (cause) => new TransactionInputError({ message: "Failed to decode from CBOR bytes", cause }) + ) + + /** + * Convert CBOR hex string to TransactionInput using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.decode(FromCBORHex(options))(hex), + (cause) => new TransactionInputError({ message: "Failed to decode from CBOR hex", cause }) + ) + + /** + * Convert TransactionInput to CBOR bytes using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORBytes = (input: TransactionInput, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(input), + (cause) => new TransactionInputError({ message: "Failed to encode to CBOR bytes", cause }) + ) + + /** + * Convert TransactionInput to CBOR hex string using Effect + * + * @since 2.0.0 + * @category conversion + */ + export const toCBORHex = (input: TransactionInput, options?: CBOR.CodecOptions) => + Eff.mapError( + Schema.encode(FromCBORHex(options))(input), + (cause) => new TransactionInputError({ message: "Failed to encode to CBOR hex", cause }) + ) +} + +/** + * Convert CBOR bytes to TransactionInput (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): TransactionInput => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Convert CBOR hex string to TransactionInput (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): TransactionInput => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Convert TransactionInput to CBOR bytes (unsafe) + * + * @since 2.0.0 + * @category conversion + */ +export const toCBORBytes = (input: TransactionInput, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(input, options)) + +/** + * Convert TransactionInput to CBOR hex string (unsafe) * * @since 2.0.0 - * @category encoding/decoding + * @category conversion */ -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - TransactionInputError - ) +export const toCBORHex = (input: TransactionInput, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(input, options)) diff --git a/packages/evolution/src/TransactionOutput.ts b/packages/evolution/src/TransactionOutput.ts index 6b30383f..5c2d13fa 100644 --- a/packages/evolution/src/TransactionOutput.ts +++ b/packages/evolution/src/TransactionOutput.ts @@ -1,10 +1,9 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Address from "./Address.js" import * as Bytes from "./Bytes.js" import * as Bytes32 from "./Bytes32.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as DatumOption from "./DatumOption.js" import * as ScriptRef from "./ScriptRef.js" import * as Value from "./Value.js" @@ -82,7 +81,13 @@ export class BabbageTransactionOutput extends Schema.TaggedClass +export type TransactionOutput = typeof TransactionOutput.Type + +export const ShelleyTransactionOutputCDDL = Schema.Tuple( + Schema.Uint8ArrayFromSelf, // address as bytes + Schema.encodedSchema(Value.FromCDDL), // value + Schema.optionalElement(Schema.Uint8ArrayFromSelf) // optional datum_hash as bytes +) /** * CDDL schema for Shelley transaction outputs @@ -94,19 +99,15 @@ export type TransactionOutput = Schema.Schema.Type * @since 2.0.0 * @category schemas */ -export const ShelleyTransactionOutputCDDLSchema = Schema.transformOrFail( - Schema.Tuple( - Schema.Uint8ArrayFromSelf, // address as bytes - Schema.encodedSchema(Value.ValueCDDLSchema), // value - Schema.optionalElement(Schema.Uint8ArrayFromSelf) // optional datum_hash as bytes - ), +export const FromShelleyTransactionOutputCDDLSchema = Schema.transformOrFail( + ShelleyTransactionOutputCDDL, Schema.typeSchema(ShelleyTransactionOutput), { strict: true, encode: (toI) => - Effect.gen(function* () { + Eff.gen(function* () { const addressBytes = yield* ParseResult.encode(Address.FromBytes)(toI.address) - const valueBytes = yield* ParseResult.encode(Value.ValueCDDLSchema)(toI.amount) + const valueBytes = yield* ParseResult.encode(Value.FromCDDL)(toI.amount) if (toI.datumHash !== undefined) { const hashBytes = yield* ParseResult.encode(Bytes.FromBytes)(toI.datumHash) @@ -116,11 +117,11 @@ export const ShelleyTransactionOutputCDDLSchema = Schema.transformOrFail( return [addressBytes, valueBytes] as const }), decode: (fromI) => - Effect.gen(function* () { + Eff.gen(function* () { const [addressBytes, valueBytes, datumHashBytes] = fromI const address = yield* ParseResult.decode(Address.FromBytes)(addressBytes) - const amount = yield* ParseResult.decode(Value.ValueCDDLSchema)(valueBytes) + const amount = yield* ParseResult.decode(Value.FromCDDL)(valueBytes) let datumHash: string | undefined if (datumHashBytes !== undefined) { @@ -136,6 +137,13 @@ export const ShelleyTransactionOutputCDDLSchema = Schema.transformOrFail( } ) +const BabbageTransactionOutputCDDL = Schema.Struct({ + 0: Schema.Uint8ArrayFromSelf, // address as bytes + 1: Schema.encodedSchema(Value.FromCDDL), // value + 2: Schema.optional(Schema.encodedSchema(DatumOption.DatumOptionCDDLSchema)), // optional datum_option + 3: Schema.optional(Schema.encodedSchema(ScriptRef.FromCDDL)) // optional script_ref +}) + /** * CDDL schema for Babbage transaction outputs * @@ -146,20 +154,15 @@ export const ShelleyTransactionOutputCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const BabbageTransactionOutputCDDLSchema = Schema.transformOrFail( - Schema.Struct({ - 0: Schema.Uint8ArrayFromSelf, // address as bytes - 1: Schema.encodedSchema(Value.ValueCDDLSchema), // value - 2: Schema.optional(Schema.encodedSchema(DatumOption.DatumOptionCDDLSchema)), // optional datum_option - 3: Schema.optional(Schema.encodedSchema(ScriptRef.FromCDDL)) // optional script_ref - }), +export const FromBabbageTransactionOutputCDDLSchema = Schema.transformOrFail( + BabbageTransactionOutputCDDL, Schema.typeSchema(BabbageTransactionOutput), { strict: true, encode: (toI) => - Effect.gen(function* () { + Eff.gen(function* () { const addressBytes = yield* ParseResult.encode(Address.FromBytes)(toI.address) - const valueBytes = yield* ParseResult.encode(Value.ValueCDDLSchema)(toI.amount) + const valueBytes = yield* ParseResult.encode(Value.FromCDDL)(toI.amount) // Prepare optional fields const datumOptionBytes = @@ -179,7 +182,7 @@ export const BabbageTransactionOutputCDDLSchema = Schema.transformOrFail( } }), decode: (fromI) => - Effect.gen(function* () { + Eff.gen(function* () { const addressBytes = fromI[0] const valueBytes = fromI[1] const datumOptionBytes = fromI[2] @@ -190,7 +193,7 @@ export const BabbageTransactionOutputCDDLSchema = Schema.transformOrFail( } const address = yield* ParseResult.decode(Address.FromBytes)(addressBytes) - const amount = yield* ParseResult.decode(Value.ValueCDDLSchema)(valueBytes) + const amount = yield* ParseResult.decode(Value.FromCDDL)(valueBytes) let datumOption: DatumOption.DatumOption | undefined if (datumOptionBytes !== undefined) { @@ -212,6 +215,8 @@ export const BabbageTransactionOutputCDDLSchema = Schema.transformOrFail( } ) +export const CDDLSchema = Schema.Union(ShelleyTransactionOutputCDDL, BabbageTransactionOutputCDDL) + /** * CDDL schema for transaction outputs * @@ -224,42 +229,202 @@ export const BabbageTransactionOutputCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const TransactionOutputCDDLSchema = Schema.Union( - ShelleyTransactionOutputCDDLSchema, - BabbageTransactionOutputCDDLSchema +export const FromTransactionOutputCDDLSchema = Schema.Union( + FromShelleyTransactionOutputCDDLSchema, + FromBabbageTransactionOutputCDDLSchema ) /** * CBOR bytes transformation schema for TransactionOutput. - * Transforms between Uint8Array and TransactionOutput using CBOR encoding. + * Transforms between CBOR bytes and TransactionOutput. * * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - TransactionOutputCDDLSchema // CBOR → TransactionOutput - ) + FromTransactionOutputCDDLSchema // CBOR → TransactionOutput + ).annotations({ + identifier: "TransactionOutput.FromCBORBytes", + title: "TransactionOutput from CBOR Bytes", + description: "Transforms CBOR bytes (Uint8Array) to TransactionOutput" + }) /** * CBOR hex transformation schema for TransactionOutput. - * Transforms between hex string and TransactionOutput using CBOR encoding. + * Transforms between CBOR hex string and TransactionOutput. * * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → TransactionOutput - ) + FromCBORBytes(options) // Uint8Array → TransactionOutput + ).annotations({ + identifier: "TransactionOutput.FromCBORHex", + title: "TransactionOutput from CBOR Hex", + description: "Transforms CBOR hex string to TransactionOutput" + }) + +/** + * Check if two TransactionOutput instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: TransactionOutput, b: TransactionOutput): boolean => { + if (a._tag !== b._tag) return false + + if (a._tag === "ShelleyTransactionOutput" && b._tag === "ShelleyTransactionOutput") { + return a.address === b.address && a.amount === b.amount && a.datumHash === b.datumHash + } + + if (a._tag === "BabbageTransactionOutput" && b._tag === "BabbageTransactionOutput") { + return ( + a.address === b.address && a.amount === b.amount && a.datumOption === b.datumOption && a.scriptRef === b.scriptRef + ) + } -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - TransactionOutputError + return false +} + +/** + * Create a Shelley transaction output. + * + * @since 2.0.0 + * @category constructors + */ +export const makeShelley = ( + address: Address.Address, + amount: Value.Value, + datumHash?: string +): ShelleyTransactionOutput => new ShelleyTransactionOutput({ address, amount, datumHash }) + +/** + * Create a Babbage transaction output. + * + * @since 2.0.0 + * @category constructors + */ +export const makeBabbage = ( + address: Address.Address, + amount: Value.Value, + datumOption?: DatumOption.DatumOption, + scriptRef?: ScriptRef.ScriptRef +): BabbageTransactionOutput => new BabbageTransactionOutput({ address, amount, datumOption, scriptRef }) + +/** + * @since 2.0.0 + * @category FastCheck + */ +export const arbitrary = (): FastCheck.Arbitrary => + FastCheck.constant( + // Return a basic instance that will be properly typed by the schema + {} as TransactionOutput ) + +/** + * Effect namespace containing schema decode and encode operations. + * + * @since 2.0.0 + * @category Effect + */ +export namespace Effect { + /** + * Parse a TransactionOutput from CBOR bytes using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = ( + input: Uint8Array, + options?: CBOR.CodecOptions + ): Eff.Effect => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(input), + (cause) => new TransactionOutputError({ message: "Failed to decode TransactionOutput from CBOR bytes", cause }) + ) + + /** + * Parse a TransactionOutput from CBOR hex using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = ( + input: string, + options?: CBOR.CodecOptions + ): Eff.Effect => + Eff.mapError( + Schema.decode(FromCBORHex(options))(input), + (cause) => new TransactionOutputError({ message: "Failed to decode TransactionOutput from CBOR hex", cause }) + ) + + /** + * Convert a TransactionOutput to CBOR bytes using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = ( + value: TransactionOutput, + options?: CBOR.CodecOptions + ): Eff.Effect => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(value), + (cause) => new TransactionOutputError({ message: "Failed to encode TransactionOutput to CBOR bytes", cause }) + ) + + /** + * Convert a TransactionOutput to CBOR hex using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = ( + value: TransactionOutput, + options?: CBOR.CodecOptions + ): Eff.Effect => + Eff.mapError( + Schema.encode(FromCBORHex(options))(value), + (cause) => new TransactionOutputError({ message: "Failed to encode TransactionOutput to CBOR hex", cause }) + ) +} + +/** + * Convert TransactionOutput to CBOR bytes (unsafe). + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (value: TransactionOutput, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(value, options)) + +/** + * Convert TransactionOutput to CBOR hex (unsafe). + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (value: TransactionOutput, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(value, options)) + +/** + * Parse TransactionOutput from CBOR bytes (unsafe). + * + * @since 2.0.0 + * @category decoding + */ +export const fromCBORBytes = (value: Uint8Array, options?: CBOR.CodecOptions): TransactionOutput => + Eff.runSync(Effect.fromCBORBytes(value, options)) + +/** + * Parse TransactionOutput from CBOR hex (unsafe). + * + * @since 2.0.0 + * @category decoding + */ +export const fromCBORHex = (value: string, options?: CBOR.CodecOptions): TransactionOutput => + Eff.runSync(Effect.fromCBORHex(value, options)) diff --git a/packages/evolution/src/UnitInterval.ts b/packages/evolution/src/UnitInterval.ts index f3d409fd..b14751ac 100644 --- a/packages/evolution/src/UnitInterval.ts +++ b/packages/evolution/src/UnitInterval.ts @@ -61,6 +61,17 @@ export const UnitInterval = Schema.Struct({ export type UnitInterval = typeof UnitInterval.Type +/** + * Smart constructor for creating UnitInterval values. + * Validates that denominator > 0 and numerator <= denominator. + * + * @since 2.0.0 + * @category constructors + */ +export const make = UnitInterval.make + +export const CDDLSchema = CBOR.Tag + /** * CDDL schema for UnitInterval following the Conway specification. * @@ -73,7 +84,7 @@ export type UnitInterval = typeof UnitInterval.Type * @since 2.0.0 * @category schemas */ -export const FromCDDL = Schema.transformOrFail(CBOR.Tag, UnitInterval, { +export const FromCDDL = Schema.transformOrFail(CDDLSchema, UnitInterval, { strict: true, encode: (_, __, ___, unitInterval) => Effect.succeed( @@ -117,7 +128,7 @@ export const FromCDDL = Schema.transformOrFail(CBOR.Tag, UnitInterval, { * @since 2.0.0 * @category schemas */ -export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR FromCDDL // CBOR → UnitInterval @@ -132,7 +143,7 @@ export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) * @since 2.0.0 * @category schemas */ -export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array FromCBORBytes(options) // Uint8Array → UnitInterval @@ -173,12 +184,12 @@ export const fromBigDecimal = (value: BigDecimal.BigDecimal): UnitInterval => { } /** - * Generate a random UnitInterval. + * FastCheck arbitrary for generating random UnitInterval instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.bigInt({ min: 1n, max: 1000000n }).chain((denominator) => +export const arbitrary = FastCheck.bigInt({ min: 1n, max: 1000000n }).chain((denominator) => FastCheck.bigInt({ min: 0n, max: denominator }).map((numerator) => UnitInterval.make({ numerator, denominator })) ) @@ -188,7 +199,7 @@ export const generator = FastCheck.bigInt({ min: 1n, max: 1000000n }).chain((den * @since 2.0.0 * @category codecs */ -export const CBORCodec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const CBORCodec = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => createEncoders( { cborBytes: FromCBORBytes(options), diff --git a/packages/evolution/src/Url.ts b/packages/evolution/src/Url.ts index 44fd2947..a7e3dd52 100644 --- a/packages/evolution/src/Url.ts +++ b/packages/evolution/src/Url.ts @@ -1,6 +1,5 @@ -import { Data, Schema } from "effect" +import { Data, Effect as Eff, Schema } from "effect" -import * as _Codec from "./Codec.js" import * as Text128 from "./Text128.js" /** @@ -21,6 +20,7 @@ export const URL_MAX_LENGTH = 128 export class UrlError extends Data.TaggedError("UrlError")<{ message?: string reason?: "InvalidLength" | "InvalidFormat" | "TooLong" + cause?: unknown }> {} /** @@ -65,17 +65,137 @@ export const FromHex = Schema.compose( export const equals = (a: Url, b: Url): boolean => a === b /** - * Generate a random Url. + * Check if the given value is a valid Url * * @since 2.0.0 - * @category generators + * @category predicates */ -export const generator = Text128.generator.map((text) => Url.make(text)) - -export const Codec = _Codec.createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - UrlError -) +export const isUrl = Schema.is(Url) + +/** + * FastCheck arbitrary for generating random Url instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = Text128.arbitrary.map((text) => Url.make(text)) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse Url from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): Url => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse Url from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): Url => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode Url to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (url: Url): Uint8Array => + Eff.runSync(Effect.toBytes(url)) + +/** + * Encode Url to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (url: Url): string => + Eff.runSync(Effect.toHex(url)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Url from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new UrlError({ + message: "Failed to parse Url from bytes", + cause + }) + ) + ) + + /** + * Parse Url from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new UrlError({ + message: "Failed to parse Url from hex", + cause + }) + ) + ) + + /** + * Encode Url to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (url: Url): Eff.Effect => + Schema.encode(FromBytes)(url).pipe( + Eff.mapError( + (cause) => + new UrlError({ + message: "Failed to encode Url to bytes", + cause + }) + ) + ) + + /** + * Encode Url to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (url: Url): Eff.Effect => + Schema.encode(FromHex)(url).pipe( + Eff.mapError( + (cause) => + new UrlError({ + message: "Failed to encode Url to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/VKey.ts b/packages/evolution/src/VKey.ts index a37096e6..f98a0e89 100644 --- a/packages/evolution/src/VKey.ts +++ b/packages/evolution/src/VKey.ts @@ -1,7 +1,9 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" +import sodium from "libsodium-wrappers-sumo" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" +import type { PrivateKey } from "./PrivateKey.js" +import { FromBytes as PrivateKeyFromBytes } from "./PrivateKey.js" /** * Error class for VKey related operations. @@ -22,7 +24,7 @@ export class VKeyError extends Data.TaggedError("VKeyError")<{ * @since 2.0.0 * @category schemas */ -export const VKey = pipe(Bytes32.HexSchema, Schema.brand("VKey")).annotations({ +export const VKey = Bytes32.HexSchema.pipe(Schema.brand("VKey")).annotations({ identifier: "VKey" }) @@ -42,6 +44,14 @@ export const FromHex = Schema.compose( identifier: "VKey.Hex" }) +/** + * Smart constructor for VKey that validates and applies branding. + * + * @since 2.0.0 + * @category constructors + */ +export const make = VKey.make + /** * Check if two VKey instances are equal. * @@ -51,26 +61,227 @@ export const FromHex = Schema.compose( export const equals = (a: VKey, b: VKey): boolean => a === b /** - * Generate a random VKey. + * Check if the given value is a valid VKey + * + * @since 2.0.0 + * @category predicates + */ +export const isVKey = Schema.is(VKey) + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse a VKey from raw bytes. + * Expects exactly 32 bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): VKey => Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse a VKey from a hex string. + * Expects exactly 64 hex characters (32 bytes). + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): VKey => Eff.runSync(Effect.fromHex(hex)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert a VKey to raw bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (vkey: VKey): Uint8Array => Eff.runSync(Effect.toBytes(vkey)) + +/** + * Convert a VKey to a hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (vkey: VKey): string => vkey // Already a hex string + +/** + * FastCheck arbitrary for generating random VKey instances. + * Used for property-based testing to generate valid test data. + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary: FastCheck.Arbitrary = FastCheck + .uint8Array({ minLength: Bytes32.BYTES_LENGTH, maxLength: Bytes32.BYTES_LENGTH }) + .map(fromBytes) + +// ============================================================================ +// Cryptographic Operations +// ============================================================================ + +/** + * Create a VKey from a PrivateKey (sync version that throws VKeyError). + * For extended keys (64 bytes), uses CML-compatible Ed25519-BIP32 algorithm. + * For normal keys (32 bytes), uses standard Ed25519. + * + * @since 2.0.0 + * @category cryptography + */ +export const fromPrivateKey = (privateKey: PrivateKey): VKey => { + const privateKeyBytes = Schema.encodeSync(PrivateKeyFromBytes)(privateKey) + + let publicKeyBytes: Uint8Array + if (privateKeyBytes.length === 64) { + // CML-compatible extended private key: use first 32 bytes as scalar + const scalar = privateKeyBytes.slice(0, 32) + publicKeyBytes = sodium.crypto_scalarmult_ed25519_base_noclamp(scalar) + } else { + // Standard 32-byte Ed25519 private key using sodium + publicKeyBytes = sodium.crypto_sign_seed_keypair(privateKeyBytes).publicKey + } + + return Schema.decodeSync(FromBytes)(publicKeyBytes) +} + +/** + * Create a VKey from a PrivateKey using Effect error handling. + * For extended keys (64 bytes), uses CML-compatible Ed25519-BIP32 algorithm. + * For normal keys (32 bytes), uses standard Ed25519. + * + * @since 2.0.0 + * @category cryptography + */ +const fromPrivateKeyEffect = (privateKey: PrivateKey): Eff.Effect => + Eff.gen(function* () { + const privateKeyBytes = yield* Schema.encode(PrivateKeyFromBytes)(privateKey).pipe( + Eff.mapError( + (cause) => + new VKeyError({ + message: "Failed to encode private key to bytes", + cause + }) + ) + ) + + const publicKeyBytes = yield* Eff.try({ + try: () => { + if (privateKeyBytes.length === 64) { + // CML-compatible extended private key: use first 32 bytes as scalar + const scalar = privateKeyBytes.slice(0, 32) + return sodium.crypto_scalarmult_ed25519_base_noclamp(scalar) + } else { + // Standard 32-byte Ed25519 private key using sodium + return sodium.crypto_sign_seed_keypair(privateKeyBytes).publicKey + } + }, + catch: (cause) => + new VKeyError({ + message: "Failed to derive public key from private key", + cause + }) + }) + + return yield* Schema.decode(FromBytes)(publicKeyBytes).pipe( + Eff.mapError( + (cause) => + new VKeyError({ + message: "Failed to create VKey from public key bytes", + cause + }) + ) + ) + }) + +/** + * Verify a signature against a message using this verification key. * * @since 2.0.0 - * @category generators + * @category cryptography */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes32.Bytes32_BYTES_LENGTH, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const verify = ( + vkey: VKey, + message: Uint8Array, + signature: Uint8Array +): boolean => { + // Convert VKey to bytes + const publicKeyBytes = toBytes(vkey) + return sodium.crypto_sign_verify_detached(signature, message, publicKeyBytes) +} + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ /** - * Codec utilities for VKey encoding and decoding operations. + * Effect-based error handling variants for functions that can fail. + * Returns Effect for composable error handling. * * @since 2.0.0 - * @category encoding/decoding + * @category effect */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - VKeyError -) +export namespace Effect { + /** + * Parse a VKey from raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new VKeyError({ + message: "Failed to parse VKey from bytes", + cause + }) + ) + ) + + /** + * Parse a VKey from a hex string using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new VKeyError({ + message: "Failed to parse VKey from hex", + cause + }) + ) + ) + + /** + * Convert a VKey to raw bytes using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (vkey: VKey): Eff.Effect => + Schema.encode(FromBytes)(vkey).pipe( + Eff.mapError( + (cause) => + new VKeyError({ + message: "Failed to encode VKey to bytes", + cause + }) + ) + ) + + /** + * Create a VKey from a PrivateKey using Effect error handling. + * + * @since 2.0.0 + * @category cryptography + */ + export const fromPrivateKey = fromPrivateKeyEffect +} diff --git a/packages/evolution/src/Value.ts b/packages/evolution/src/Value.ts index aea98df4..437465e1 100644 --- a/packages/evolution/src/Value.ts +++ b/packages/evolution/src/Value.ts @@ -1,9 +1,8 @@ -import { Data, Effect, FastCheck, Option, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Option, ParseResult, Schema } from "effect" import * as AssetName from "./AssetName.js" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" import * as Coin from "./Coin.js" import * as MultiAsset from "./MultiAsset.js" import * as PolicyId from "./PolicyId.js" @@ -35,12 +34,12 @@ export class ValueError extends Data.TaggedError("ValueError")<{ * @category schemas */ export class OnlyCoin extends Schema.TaggedClass("OnlyCoin")("OnlyCoin", { - coin: Coin.CoinSchema + coin: Coin.Coin }) {} export class WithAssets extends Schema.TaggedClass("WithAssets")("WithAssets", { - coin: Coin.CoinSchema, - assets: MultiAsset.MultiAssetSchema + coin: Coin.Coin, + assets: MultiAsset.MultiAsset }) {} export const Value = Schema.Union(OnlyCoin, WithAssets) @@ -223,18 +222,28 @@ export const is = (value: unknown): value is Value => Schema.is(Value)(value) * @since 2.0.0 * @category generators */ -export const generator = FastCheck.oneof( +export const arbitrary = FastCheck.oneof( FastCheck.record({ _tag: FastCheck.constant("OnlyCoin"), - coin: Coin.generator + coin: Coin.arbitrary }), FastCheck.record({ _tag: FastCheck.constant("WithAssets"), - coin: Coin.generator, - assets: MultiAsset.generator + coin: Coin.arbitrary, + assets: MultiAsset.arbitrary }) ) +export const CDDLSchema = Schema.Union( + CBOR.Integer, + Schema.Tuple( + CBOR.Integer, + Schema.encodedSchema( + MultiAsset.MultiAssetCDDLSchema // MultiAsset CDDL structure + ) + ) +) + /** * CDDL schema for Value as union structure. * @@ -249,81 +258,69 @@ export const generator = FastCheck.oneof( * @since 2.0.0 * @category schemas */ -export const ValueCDDLSchema = Schema.transformOrFail( - Schema.Union( - CBOR.Integer, - Schema.Tuple( - CBOR.Integer, - Schema.encodedSchema( - MultiAsset.MultiAssetCDDLSchema // MultiAsset CDDL structure - ) - ) - ), - Schema.typeSchema(Value), - { - strict: true, - encode: (toI) => - Effect.gen(function* () { - // expected encode result - // readonly [bigint, readonly (readonly [Uint8Array, readonly (readonly [Uint8Array, bigint])[]])[]] - if (toI._tag === "OnlyCoin") { - // This is OnlyCoin, encode just the coin amount - return toI.coin - } else { - // Value with assets (WithAssets) - // Convert MultiAsset to raw Map data for CBOR encoding - const outerMap = new Map>() - - for (const [policyId, assetMap] of toI.assets.entries()) { - const policyIdBytes = yield* ParseResult.encode(PolicyId.FromBytes)(policyId) - const innerMap = new Map() - - for (const [assetName, amount] of assetMap.entries()) { - const assetNameBytes = yield* ParseResult.encode(AssetName.FromBytes)(assetName) - innerMap.set(assetNameBytes, amount) - } - - outerMap.set(policyIdBytes, innerMap) +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Value), { + strict: true, + encode: (toI) => + Eff.gen(function* () { + // expected encode result + // readonly [bigint, readonly (readonly [Uint8Array, readonly (readonly [Uint8Array, bigint])[]])[]] + if (toI._tag === "OnlyCoin") { + // This is OnlyCoin, encode just the coin amount + return toI.coin + } else { + // Value with assets (WithAssets) + // Convert MultiAsset to raw Map data for CBOR encoding + const outerMap = new Map>() + + for (const [policyId, assetMap] of toI.assets.entries()) { + const policyIdBytes = yield* ParseResult.encode(PolicyId.FromBytes)(policyId) + const innerMap = new Map() + + for (const [assetName, amount] of assetMap.entries()) { + const assetNameBytes = yield* ParseResult.encode(AssetName.FromBytes)(assetName) + innerMap.set(assetNameBytes, amount) } - return [toI.coin, outerMap] as const // Return as tuple + outerMap.set(policyIdBytes, innerMap) } - }), - decode: (fromA) => - Effect.gen(function* () { - if (typeof fromA === "bigint") { - // ADA-only value - create OnlyCoin instance - return new OnlyCoin({ - coin: fromA - }) - } else { - // Value with assets [coin, multiasset] - const [coinAmount, multiAssetCddl] = fromA - - // Convert from CDDL format to MultiAsset manually - const result = new Map() - for (const [policyIdBytes, assetMapCddl] of multiAssetCddl.entries()) { - const policyId = yield* ParseResult.decode(PolicyId.FromBytes)(policyIdBytes) - - const assetMap = new Map() - for (const [assetNameBytes, amount] of assetMapCddl.entries()) { - const assetName = yield* ParseResult.decode(AssetName.FromBytes)(assetNameBytes) - const positiveCoin = PositiveCoin.make(amount) - assetMap.set(assetName, positiveCoin) - } - - result.set(policyId, assetMap) + return [toI.coin, outerMap] as const // Return as tuple + } + }), + decode: (fromA) => + Eff.gen(function* () { + if (typeof fromA === "bigint") { + // ADA-only value - create OnlyCoin instance + return new OnlyCoin({ + coin: Coin.make(fromA) + }) + } else { + // Value with assets [coin, multiasset] + const [coinAmount, multiAssetCddl] = fromA + + // Convert from CDDL format to MultiAsset manually + const result = new Map() + + for (const [policyIdBytes, assetMapCddl] of multiAssetCddl.entries()) { + const policyId = yield* ParseResult.decode(PolicyId.FromBytes)(policyIdBytes) + + const assetMap = new Map() + for (const [assetNameBytes, amount] of assetMapCddl.entries()) { + const assetName = yield* ParseResult.decode(AssetName.FromBytes)(assetNameBytes) + const positiveCoin = PositiveCoin.make(amount) + assetMap.set(assetName, positiveCoin) } - return new WithAssets({ - coin: coinAmount, - assets: result - }) + result.set(policyId, assetMap) } - }) - } -) + + return new WithAssets({ + coin: Coin.make(coinAmount), + assets: MultiAsset.make(result) + }) + } + }) +}) /** * TypeScript type for the raw CDDL representation. @@ -332,37 +329,176 @@ export const ValueCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category model */ -export type ValueCDDL = typeof ValueCDDLSchema.Type +export type ValueCDDL = typeof FromCDDL.Type /** * CBOR bytes transformation schema for Value. + * Transforms between CBOR bytes and Value using CBOR encoding. * * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - ValueCDDLSchema // CBOR → Value - ) + FromCDDL // CBOR → Value + ).annotations({ + identifier: "Value.FromCBORBytes", + title: "Value from CBOR Bytes", + description: "Transforms CBOR bytes to Value" + }) /** * CBOR hex transformation schema for Value. + * Transforms between CBOR hex string and Value using CBOR encoding. * * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → Value - ) + FromCBORBytes(options) // Uint8Array → Value + ).annotations({ + identifier: "Value.FromCBORHex", + title: "Value from CBOR Hex", + description: "Transforms CBOR hex string to Value" + }) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - ValueError - ) +/** + * Legacy alias for FromCBORBytes - kept for backwards compatibility. + * + * @since 2.0.0 + * @category schemas + * @deprecated Use FromCBORBytes instead + */ +export const FromBytes = FromCBORBytes + +/** + * Legacy alias for FromCBORHex - kept for backwards compatibility. + * + * @since 2.0.0 + * @category schemas + * @deprecated Use FromCBORHex instead + */ +export const FromHex = FromCBORHex + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse Value from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Value => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse Value from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Value => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Encode Value to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (value: Value, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(value, options)) + +/** + * Encode Value to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (value: Value, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(value, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Value from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause: unknown) => + new ValueError({ + message: "Failed to parse Value from CBOR bytes", + cause + }) + ) + ) + + /** + * Parse Value from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause: unknown) => + new ValueError({ + message: "Failed to parse Value from CBOR hex", + cause + }) + ) + ) + + /** + * Encode Value to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (value: Value, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORBytes(options))(value).pipe( + Eff.mapError( + (cause: unknown) => + new ValueError({ + message: "Failed to encode Value to CBOR bytes", + cause + }) + ) + ) + + /** + * Encode Value to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (value: Value, options?: CBOR.CodecOptions): Eff.Effect => + Schema.encode(FromCBORHex(options))(value).pipe( + Eff.mapError( + (cause: unknown) => + new ValueError({ + message: "Failed to encode Value to CBOR hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/VotingProcedures.ts b/packages/evolution/src/VotingProcedures.ts index 553c0345..03ea394e 100644 --- a/packages/evolution/src/VotingProcedures.ts +++ b/packages/evolution/src/VotingProcedures.ts @@ -1,20 +1,773 @@ -import { Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" + +import * as Anchor from "./Anchor.js" +import * as Bytes from "./Bytes.js" +import * as CBOR from "./CBOR.js" +import * as Credential from "./Credential.js" +import * as DRep from "./DRep.js" +import * as GovernanceAction from "./GovernanceAction.js" +import * as PoolKeyHash from "./PoolKeyHash.js" + +/** + * Error class for VotingProcedures related operations. + * + * @since 2.0.0 + * @category errors + */ +export class VotingProceduresError extends Data.TaggedError("VotingProceduresError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Voter types based on Conway CDDL specification. + * + * ``` + * voter = + * [ 0, committee_hot_credential ] // Constitutional Committee + * / [ 1, drep ] // DRep + * / [ 2, pool_keyhash ] // Stake Pool Operator + * ``` + * + * @since 2.0.0 + * @category schemas + */ +export const ConstitutionalCommitteeVoter = Schema.TaggedStruct("ConstitutionalCommitteeVoter", { + credential: Credential.Credential +}) + +export const DRepVoter = Schema.TaggedStruct("DRepVoter", { + drep: DRep.DRep +}) + +export const StakePoolVoter = Schema.TaggedStruct("StakePoolVoter", { + poolKeyHash: PoolKeyHash.PoolKeyHash +}) + +/** + * Voter union schema. + * + * @since 2.0.0 + * @category schemas + */ +export const Voter = Schema.Union( + ConstitutionalCommitteeVoter, + DRepVoter, + StakePoolVoter +) + +export type Voter = Schema.Schema.Type + +/** + * CDDL schema for Voter as tuple structure. + * Maps to: [voter_type, voter_data] + * + * @since 2.0.0 + * @category schemas + */ +export const VoterCDDL = Schema.Union( + Schema.Tuple(Schema.Literal(0), Credential.CDDLSchema), // committee_hot_credential + Schema.Tuple(Schema.Literal(1), DRep.CDDLSchema), // drep + Schema.Tuple(Schema.Literal(2), CBOR.ByteArray) // pool_keyhash +) + +/** + * CDDL transformation schema for Voter. + * + * @since 2.0.0 + * @category schemas + */ +export const VoterFromCDDL = Schema.transformOrFail(VoterCDDL, Schema.typeSchema(Voter), { + strict: true, + encode: (voter) => + Eff.gen(function* () { + switch (voter._tag) { + case "ConstitutionalCommitteeVoter": { + const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(voter.credential) + return [0, credentialCDDL] as const + } + case "DRepVoter": { + const drepCDDL = yield* ParseResult.encode(DRep.FromCDDL)(voter.drep) + return [1, drepCDDL] as const + } + case "StakePoolVoter": { + const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(voter.poolKeyHash) + return [2, poolKeyHashBytes] as const + } + } + }), + decode: (cddl) => + Eff.gen(function* () { + const [voterType, voterData] = cddl + switch (voterType) { + case 0: { + const credential = yield* ParseResult.decode(Credential.FromCDDL)(voterData) + return { _tag: "ConstitutionalCommitteeVoter", credential } as const + } + case 1: { + const drep = yield* ParseResult.decode(DRep.FromCDDL)(voterData) + return { _tag: "DRepVoter", drep } as const + } + case 2: { + const poolKeyHash = yield* ParseResult.decode(PoolKeyHash.FromBytes)(voterData) + return { _tag: "StakePoolVoter", poolKeyHash } as const + } + default: + return yield* ParseResult.fail(new ParseResult.Type(VoterCDDL.ast, cddl)) + } + }) +}) + +/** + * Vote types based on Conway CDDL specification. + * + * ``` + * vote = 0 / 1 / 2 ; No / Yes / Abstain + * ``` + * + * @since 2.0.0 + * @category schemas + */ +export class NoVote extends Schema.TaggedClass()("NoVote", {}) {} +export class YesVote extends Schema.TaggedClass()("YesVote", {}) {} +export class AbstainVote extends Schema.TaggedClass()("AbstainVote", {}) {} + +/** + * Vote union schema. + * + * @since 2.0.0 + * @category schemas + */ +export const Vote = Schema.Union(NoVote, YesVote, AbstainVote) + +export type Vote = typeof Vote.Type + +/** + * CDDL schema for Vote. + * + * @since 2.0.0 + * @category schemas + */ +export const VoteCDDL = Schema.Union( + Schema.Literal(0n), // No + Schema.Literal(1n), // Yes + Schema.Literal(2n) // Abstain +) + +/** + * CDDL transformation schema for Vote. + * + * @since 2.0.0 + * @category schemas + */ +export const VoteFromCDDL = Schema.transformOrFail(VoteCDDL, Schema.typeSchema(Vote), { + strict: true, + encode: (vote) => + Eff.gen(function* () { + switch (vote._tag) { + case "NoVote": + return 0n as const + case "YesVote": + return 1n as const + case "AbstainVote": + return 2n as const + } + }), + decode: (cddl) => + Eff.gen(function* () { + switch (cddl) { + case 0n: + return new NoVote() + case 1n: + return new YesVote() + case 2n: + return new AbstainVote() + default: + return yield* ParseResult.fail(new ParseResult.Type(VoteCDDL.ast, cddl)) + } + }) +}) + +/** + * Voting procedure based on Conway CDDL specification. + * + * ``` + * voting_procedure = [ vote, anchor / null ] + * ``` + * + * @since 2.0.0 + * @category schemas + */ +export class VotingProcedure extends Schema.Class("VotingProcedure")({ + vote: Vote, + anchor: Schema.NullOr(Anchor.Anchor) +}) {} + +/** + * CDDL schema for VotingProcedure tuple structure. + * + * @since 2.0.0 + * @category schemas + */ +export const VotingProcedureCDDL = Schema.Tuple( + VoteCDDL, // vote + Schema.NullOr(Anchor.CDDLSchema) // anchor / null +) + +/** + * CDDL transformation schema for VotingProcedure. + * + * @since 2.0.0 + * @category schemas + */ +export const VotingProcedureFromCDDL = Schema.transformOrFail(VotingProcedureCDDL, Schema.typeSchema(VotingProcedure), { + strict: true, + encode: (procedure) => + Eff.gen(function* () { + const voteCDDL = yield* ParseResult.encode(VoteFromCDDL)(procedure.vote) + const anchorCDDL = procedure.anchor ? yield* ParseResult.encode(Anchor.FromCDDL)(procedure.anchor) : null + return [voteCDDL, anchorCDDL] as const + }), + decode: ([voteCDDL, anchorCDDL]) => + Eff.gen(function* () { + const vote = yield* ParseResult.decode(VoteFromCDDL)(voteCDDL) + const anchor = anchorCDDL ? yield* ParseResult.decode(Anchor.FromCDDL)(anchorCDDL) : null + return new VotingProcedure({ vote, anchor }) + }) +}) /** - * VotingProcedures based on Conway CDDL specification + * VotingProcedures based on Conway CDDL specification. * * ``` - * CDDL: voting_procedures = {+ voter => {+ gov_action_id => voting_procedure}} + * voting_procedures = {+ voter => {+ gov_action_id => voting_procedure}} * ``` * - * This is a complex nested map structure that we'll implement gradually - * as we create the required sub-types. + * A nested map structure where voters map to their votes on specific governance actions. * * @since 2.0.0 * @category model */ +export class VotingProcedures extends Schema.Class("VotingProcedures")({ + procedures: Schema.Map({ + key: Voter, + value: Schema.Map({ + key: GovernanceAction.GovActionId, + value: VotingProcedure + }) + }) +}) {} + +/** + * CDDL schema for VotingProcedures map structure. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = Schema.MapFromSelf({ + key: VoterCDDL, // voter + value: Schema.MapFromSelf({ + key: GovernanceAction.GovActionIdCDDL, // gov_action_id + value: VotingProcedureCDDL // voting_procedure + }) +}) + +/** + * CDDL transformation schema for VotingProcedures. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(VotingProcedures), { + strict: true, + encode: (toA) => + Eff.gen(function* () { + const mapEntries = new Map() + + for (const [voter, govActionMap] of toA.procedures) { + const voterCDDL = yield* ParseResult.encode(VoterFromCDDL)(voter) + const innerMapEntries = new Map() + + for (const [govActionId, votingProcedure] of govActionMap) { + const govActionIdCDDL = yield* ParseResult.encode(GovernanceAction.GovActionIdFromCDDL)(govActionId) + const procedureCDDL = yield* ParseResult.encode(VotingProcedureFromCDDL)(votingProcedure) + innerMapEntries.set(govActionIdCDDL, procedureCDDL) + } + + mapEntries.set(voterCDDL, innerMapEntries) + } + + return mapEntries + }), + decode: (fromA) => + Eff.gen(function* () { + const proceduresMap = new Map>() + + for (const [voterCDDL, innerMapCDDL] of fromA) { + const voter = yield* ParseResult.decode(VoterFromCDDL)(voterCDDL) + const govActionMap = new Map() + + for (const [govActionIdCDDL, procedureCDDL] of innerMapCDDL) { + const govActionId = yield* ParseResult.decode(GovernanceAction.GovActionIdFromCDDL)(govActionIdCDDL) + const procedure = yield* ParseResult.decode(VotingProcedureFromCDDL)(procedureCDDL) + govActionMap.set(govActionId, procedure) + } + + proceduresMap.set(voter, govActionMap) + } + + return new VotingProcedures({ procedures: proceduresMap }) + }) +}) + +/** + * CBOR bytes transformation schema for VotingProcedures. + * + * @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 → VotingProcedures + ).annotations({ + identifier: "VotingProcedures.FromCBORBytes", + title: "VotingProcedures from CBOR Bytes", + description: "Transforms CBOR bytes to VotingProcedures" + }) + +/** + * CBOR hex transformation schema for VotingProcedures. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose( + Bytes.FromHex, // string → Uint8Array + FromCBORBytes(options) // Uint8Array → VotingProcedures + ).annotations({ + identifier: "VotingProcedures.FromCBORHex", + title: "VotingProcedures from CBOR Hex", + description: "Transforms CBOR hex string to VotingProcedures" + }) + +// ============================================================================ +// Constructors +// ============================================================================ + +/** + * Create a VotingProcedures instance. + * + * @since 2.0.0 + * @category constructors + */ +export const make = (procedures: Map>): VotingProcedures => + new VotingProcedures({ procedures }) + +/** + * Create a VotingProcedure instance. + * + * @since 2.0.0 + * @category constructors + */ +export const makeProcedure = (vote: Vote, anchor?: Anchor.Anchor | null): VotingProcedure => + new VotingProcedure({ vote, anchor: anchor ?? null }) + +/** + * Create a Constitutional Committee voter. + * + * @since 2.0.0 + * @category constructors + */ +export const makeCommitteeVoter = (credential: Credential.Credential): Voter => + ({ _tag: "ConstitutionalCommitteeVoter", credential }) + +/** + * Create a DRep voter. + * + * @since 2.0.0 + * @category constructors + */ +export const makeDRepVoter = (drep: DRep.DRep): Voter => + ({ _tag: "DRepVoter", drep }) + +/** + * Create a Stake Pool voter. + * + * @since 2.0.0 + * @category constructors + */ +export const makeStakePoolVoter = (poolKeyHash: PoolKeyHash.PoolKeyHash): Voter => + ({ _tag: "StakePoolVoter", poolKeyHash }) + +/** + * Create a No vote. + * + * @since 2.0.0 + * @category constructors + */ +export const no = (): Vote => new NoVote() + +/** + * Create a Yes vote. + * + * @since 2.0.0 + * @category constructors + */ +export const yes = (): Vote => new YesVote() + +/** + * Create an Abstain vote. + * + * @since 2.0.0 + * @category constructors + */ +export const abstain = (): Vote => new AbstainVote() + +// ============================================================================ +// Type Guards +// ============================================================================ + +/** + * Check if a voter is a Constitutional Committee voter. + * + * @since 2.0.0 + * @category predicates + */ +export const isConstitutionalCommitteeVoter = (voter: Voter): voter is Schema.Schema.Type => + voter._tag === "ConstitutionalCommitteeVoter" + +/** + * Check if a voter is a DRep voter. + * + * @since 2.0.0 + * @category predicates + */ +export const isDRepVoter = (voter: Voter): voter is Schema.Schema.Type => voter._tag === "DRepVoter" + +/** + * Check if a voter is a Stake Pool voter. + * + * @since 2.0.0 + * @category predicates + */ +export const isStakePoolVoter = (voter: Voter): voter is Schema.Schema.Type => voter._tag === "StakePoolVoter" + +/** + * Check if a vote is a No vote. + * + * @since 2.0.0 + * @category predicates + */ +export const isNoVote = (vote: Vote): vote is Schema.Schema.Type => vote._tag === "NoVote" + +/** + * Check if a vote is a Yes vote. + * + * @since 2.0.0 + * @category predicates + */ +export const isYesVote = (vote: Vote): vote is Schema.Schema.Type => vote._tag === "YesVote" + +/** + * Check if a vote is an Abstain vote. + * + * @since 2.0.0 + * @category predicates + */ +export const isAbstainVote = (vote: Vote): vote is Schema.Schema.Type => vote._tag === "AbstainVote" + +// ============================================================================ +// Pattern Matching +// ============================================================================ + +/** + * Pattern match on a Voter. + * + * @since 2.0.0 + * @category pattern matching + */ +export const matchVoter = (patterns: { + ConstitutionalCommitteeVoter: (credential: Credential.Credential) => R + DRepVoter: (drep: DRep.DRep) => R + StakePoolVoter: (poolKeyHash: PoolKeyHash.PoolKeyHash) => R +}) => (voter: Voter): R => { + switch (voter._tag) { + case "ConstitutionalCommitteeVoter": + return patterns.ConstitutionalCommitteeVoter(voter.credential) + case "DRepVoter": + return patterns.DRepVoter(voter.drep) + case "StakePoolVoter": + return patterns.StakePoolVoter(voter.poolKeyHash) + } +} + +/** + * Pattern match on a Vote. + * + * @since 2.0.0 + * @category pattern matching + */ +export const matchVote = (patterns: { + NoVote: () => R + YesVote: () => R + AbstainVote: () => R +}) => (vote: Vote): R => { + switch (vote._tag) { + case "NoVote": + return patterns.NoVote() + case "YesVote": + return patterns.YesVote() + case "AbstainVote": + return patterns.AbstainVote() + } +} + +// ============================================================================ +// Equality +// ============================================================================ + +/** + * Check if two Voters are equal. + * + * @since 2.0.0 + * @category equality + */ +export const voterEquals = (a: Voter, b: Voter): boolean => { + if (a._tag !== b._tag) return false + + switch (a._tag) { + case "ConstitutionalCommitteeVoter": + return Credential.equals(a.credential, (b as Schema.Schema.Type).credential) + case "DRepVoter": + return DRep.equals(a.drep, (b as Schema.Schema.Type).drep) + case "StakePoolVoter": + return PoolKeyHash.equals(a.poolKeyHash, (b as Schema.Schema.Type).poolKeyHash) + } +} + +/** + * Check if two Votes are equal. + * + * @since 2.0.0 + * @category equality + */ +export const voteEquals = (a: Vote, b: Vote): boolean => a._tag === b._tag + +/** + * Check if two VotingProcedures are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: VotingProcedures, b: VotingProcedures): boolean => { + if (a.procedures.size !== b.procedures.size) return false + + for (const [voterA, govActionMapA] of a.procedures) { + let foundMatchingVoter = false + + for (const [voterB, govActionMapB] of b.procedures) { + if (voterEquals(voterA, voterB)) { + foundMatchingVoter = true + + if (govActionMapA.size !== govActionMapB.size) return false + + for (const [govActionIdA, procedureA] of govActionMapA) { + let foundMatchingAction = false + + for (const [govActionIdB, procedureB] of govActionMapB) { + // Simple equality for GovActionId + if (govActionIdA.transactionId === govActionIdB.transactionId && + govActionIdA.govActionIndex === govActionIdB.govActionIndex) { + foundMatchingAction = true + + const votesEqual = voteEquals(procedureA.vote, procedureB.vote) + const anchorsEqual = (procedureA.anchor === null && procedureB.anchor === null) || + (procedureA.anchor !== null && procedureB.anchor !== null && + Anchor.equals(procedureA.anchor, procedureB.anchor)) + + if (!votesEqual || !anchorsEqual) return false + break + } + } + + if (!foundMatchingAction) return false + } + + break + } + } + + if (!foundMatchingVoter) return false + } + + return true +} + +/** + * FastCheck arbitrary for VotingProcedures. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary = FastCheck.record({ + procedures: FastCheck.array( + FastCheck.tuple( + FastCheck.oneof( + FastCheck.record({ _tag: FastCheck.constant("ConstitutionalCommitteeVoter"), credential: Credential.arbitrary }), + FastCheck.record({ _tag: FastCheck.constant("DRepVoter"), drep: DRep.arbitrary }), + FastCheck.record({ _tag: FastCheck.constant("StakePoolVoter"), poolKeyHash: PoolKeyHash.arbitrary }) + ), + FastCheck.array( + FastCheck.tuple( + FastCheck.record({ + _tag: FastCheck.constant("GovActionId"), + transactionId: FastCheck.hexaString({ minLength: 64, maxLength: 64 }), + govActionIndex: FastCheck.integer({ min: 0, max: 65535 }) + }), + FastCheck.record({ + vote: FastCheck.oneof( + FastCheck.constant(new NoVote()), + FastCheck.constant(new YesVote()), + FastCheck.constant(new AbstainVote()) + ), + anchor: FastCheck.option(Anchor.arbitrary, { nil: null }) + }) + ) + ).map(arr => new Map(arr)) + ) + ).map(arr => new Map(arr)) +}) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse VotingProcedures from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): VotingProcedures => + Eff.runSync(Effect.fromCBORBytes(bytes, options) as any) + +/** + * Parse VotingProcedures from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): VotingProcedures => + Eff.runSync(Effect.fromCBORHex(hex, options) as any) + +/** + * Encode VotingProcedures to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (votingProcedures: VotingProcedures, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(votingProcedures, options) as any) + +/** + * Encode VotingProcedures to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (votingProcedures: VotingProcedures, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(votingProcedures, options) as any) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse VotingProcedures from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new VotingProceduresError({ + message: "Failed to parse VotingProcedures from bytes", + cause + }) + ) + ) + + /** + * Parse VotingProcedures from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new VotingProceduresError({ + message: "Failed to parse VotingProcedures from hex", + cause + }) + ) + ) -// TODO: Implement when Voter, GovActionId, and VotingProcedure are available -export const VotingProcedures = Schema.Unknown + /** + * Encode VotingProcedures to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = ( + votingProcedures: VotingProcedures, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORBytes(options))(votingProcedures).pipe( + Eff.mapError( + (cause) => + new VotingProceduresError({ + message: "Failed to encode VotingProcedures to bytes", + cause + }) + ) + ) -export type VotingProcedures = Schema.Schema.Type + /** + * Encode VotingProcedures to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = ( + votingProcedures: VotingProcedures, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORHex(options))(votingProcedures).pipe( + Eff.mapError( + (cause) => + new VotingProceduresError({ + message: "Failed to encode VotingProcedures to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/VrfCert.ts b/packages/evolution/src/VrfCert.ts index 9ddaf193..a790af4e 100644 --- a/packages/evolution/src/VrfCert.ts +++ b/packages/evolution/src/VrfCert.ts @@ -1,10 +1,9 @@ -import { Data, Effect, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as Bytes32 from "./Bytes32.js" import * as Bytes80 from "./Bytes80.js" import * as CBOR from "./CBOR.js" -import * as _Codec from "./Codec.js" /** * Error class for VrfCert related operations. @@ -24,7 +23,10 @@ export class VrfCertError extends Data.TaggedError("VrfCertError")<{ * @since 2.0.0 * @category schemas */ -export const VRFOutput = Bytes32.HexSchema.pipe(Schema.brand("VrfOutput")) +export const VRFOutput = Bytes32.HexSchema.pipe(Schema.brand("VrfOutput")).annotations({ + identifier: "VrfOutput", + description: "VRF output as a 32-byte hex string used in VRF certificates" +}) /** * Type alias for VRF output. @@ -69,7 +71,10 @@ export const VRFOutputHexSchema = Schema.compose( * @since 2.0.0 * @category schemas */ -export const VRFProof = Bytes80.HexSchema.pipe(Schema.brand("VrfProof")) +export const VRFProof = Bytes80.HexSchema.pipe(Schema.brand("VrfProof")).annotations({ + identifier: "VrfProof", + description: "VRF proof as an 80-byte hex string used in VRF certificates" +}) /** * Type alias for VRF proof. @@ -145,18 +150,18 @@ export const VrfCertCDDLSchema = Schema.transformOrFail( { strict: true, encode: (vrfCert) => - Effect.gen(function* () { - const outputBytes = yield* ParseResult.encode(Bytes.FromBytes)(vrfCert.output) - const proofBytes = yield* ParseResult.encode(Bytes.FromBytes)(vrfCert.proof) + Eff.gen(function* () { + const outputBytes = yield* ParseResult.encode(VRFOutputFromBytes)(vrfCert.output) + const proofBytes = yield* ParseResult.encode(VRFProofFromBytes)(vrfCert.proof) return [outputBytes, proofBytes] as const }), decode: ([outputBytes, proofBytes]) => - Effect.gen(function* () { - const output = yield* ParseResult.decode(Bytes.FromBytes)(outputBytes) - const proof = yield* ParseResult.decode(Bytes.FromBytes)(proofBytes) + Eff.gen(function* () { + const output = yield* ParseResult.decode(VRFOutputFromBytes)(outputBytes) + const proof = yield* ParseResult.decode(VRFProofFromBytes)(proofBytes) return new VrfCert({ - output: VRFOutput.make(output), - proof: VRFProof.make(proof) + output, + proof }) }) } @@ -168,11 +173,15 @@ export const VrfCertCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR VrfCertCDDLSchema // CBOR → VrfCert - ) + ).annotations({ + identifier: "VrfCert.FromCBORBytes", + title: "VrfCert from CBOR Bytes", + description: "Transforms CBOR bytes (Uint8Array) to VrfCert" + }) /** * CBOR hex transformation schema for VrfCert. @@ -180,11 +189,15 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → VrfCert - ) + FromCBORBytes(options) // Uint8Array → VrfCert + ).annotations({ + identifier: "VrfCert.FromCBORHex", + title: "VrfCert from CBOR Hex", + description: "Transforms CBOR hex string to VrfCert" + }) /** * Check if two VrfCert instances are equal. @@ -203,11 +216,111 @@ export const equals = (a: VrfCert, b: VrfCert): boolean => */ export const make = (output: VRFOutput, proof: VRFProof): VrfCert => new VrfCert({ output, proof }) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => - _Codec.createEncoders( - { - cborBytes: FromBytes(options), - cborHex: FromHex(options) - }, - VrfCertError +/** + * @since 2.0.0 + * @category FastCheck + */ +export const arbitrary = (): FastCheck.Arbitrary => + FastCheck.record({ + output: FastCheck.hexaString({ minLength: 64, maxLength: 64 }), + proof: FastCheck.hexaString({ minLength: 160, maxLength: 160 }) + }).chain(({ output, proof }) => + FastCheck.constant( + new VrfCert({ + output: Eff.runSync(Schema.decode(VRFOutput)(output)), + proof: Eff.runSync(Schema.decode(VRFProof)(proof)) + }) + ) ) + +/** + * Effect namespace containing schema decode and encode operations. + * + * @since 2.0.0 + * @category Effect + */ +export namespace Effect { + /** + * Parse a VrfCert from CBOR bytes using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = (input: Uint8Array, options?: CBOR.CodecOptions): Eff.Effect => + Eff.mapError( + Schema.decode(FromCBORBytes(options))(input), + (cause) => new VrfCertError({ message: "Failed to decode VrfCert from CBOR bytes", cause }) + ) + + /** + * Parse a VrfCert from CBOR hex using Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = (input: string, options?: CBOR.CodecOptions): Eff.Effect => + Eff.mapError( + Schema.decode(FromCBORHex(options))(input), + (cause) => new VrfCertError({ message: "Failed to decode VrfCert from CBOR hex", cause }) + ) + + /** + * Convert a VrfCert to CBOR bytes using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = (value: VrfCert, options?: CBOR.CodecOptions): Eff.Effect => + Eff.mapError( + Schema.encode(FromCBORBytes(options))(value), + (cause) => new VrfCertError({ message: "Failed to encode VrfCert to CBOR bytes", cause }) + ) + + /** + * Convert a VrfCert to CBOR hex using Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = (value: VrfCert, options?: CBOR.CodecOptions): Eff.Effect => + Eff.mapError( + Schema.encode(FromCBORHex(options))(value), + (cause) => new VrfCertError({ message: "Failed to encode VrfCert to CBOR hex", cause }) + ) +} + +/** + * Convert VrfCert to CBOR bytes (unsafe). + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (value: VrfCert, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(value, options)) + +/** + * Convert VrfCert to CBOR hex (unsafe). + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (value: VrfCert, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(value, options)) + +/** + * Parse VrfCert from CBOR bytes (unsafe). + * + * @since 2.0.0 + * @category decoding + */ +export const fromCBORBytes = (value: Uint8Array, options?: CBOR.CodecOptions): VrfCert => + Eff.runSync(Effect.fromCBORBytes(value, options)) + +/** + * Parse VrfCert from CBOR hex (unsafe). + * + * @since 2.0.0 + * @category decoding + */ +export const fromCBORHex = (value: string, options?: CBOR.CodecOptions): VrfCert => + Eff.runSync(Effect.fromCBORHex(value, options)) diff --git a/packages/evolution/src/VrfKeyHash.ts b/packages/evolution/src/VrfKeyHash.ts index ec364bb9..586ea3bd 100644 --- a/packages/evolution/src/VrfKeyHash.ts +++ b/packages/evolution/src/VrfKeyHash.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, pipe, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" /** * Error class for VrfKeyHash related operations. @@ -50,26 +49,111 @@ export const FromHex = Schema.compose( export const equals = (a: VrfKeyHash, b: VrfKeyHash): boolean => a === b /** - * Generate a random VrfKeyHash. + * FastCheck arbitrary for generating random VrfKeyHash instances. * * @since 2.0.0 - * @category generators + * @category arbitrary */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes32.Bytes32_BYTES_LENGTH, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const arbitrary = FastCheck.uint8Array({ + minLength: Bytes32.BYTES_LENGTH, + maxLength: Bytes32.BYTES_LENGTH +}).map((bytes) => Eff.runSync(Effect.fromBytes(bytes))) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse VrfKeyHash from raw bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): VrfKeyHash => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse VrfKeyHash from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): VrfKeyHash => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode VrfKeyHash to raw bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (vrfKeyHash: VrfKeyHash): Uint8Array => + Eff.runSync(Effect.toBytes(vrfKeyHash)) + +/** + * Encode VrfKeyHash to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (vrfKeyHash: VrfKeyHash): string => vrfKeyHash // Already a hex string + +// ============================================================================ +// Effect Namespace +// ============================================================================ /** - * Codec utilities for VrfKeyHash encoding and decoding operations. + * Effect-based error handling variants for functions that can fail. * * @since 2.0.0 - * @category encoding/decoding + * @category effect */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - VrfKeyHashError -) +export namespace Effect { + /** + * Parse VrfKeyHash from raw bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Eff.mapError( + Schema.decode(FromBytes)(bytes), + (cause) => + new VrfKeyHashError({ + message: "Failed to parse VrfKeyHash from bytes", + cause + }) + ) + + /** + * Parse VrfKeyHash from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Eff.mapError( + Schema.decode(VrfKeyHash)(hex), + (cause) => + new VrfKeyHashError({ + message: "Failed to parse VrfKeyHash from hex", + cause + }) + ) + + /** + * Encode VrfKeyHash to raw bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (vrfKeyHash: VrfKeyHash): Eff.Effect => + Eff.mapError( + Schema.encode(FromBytes)(vrfKeyHash), + (cause) => + new VrfKeyHashError({ + message: "Failed to encode VrfKeyHash to bytes", + cause + }) + ) +} diff --git a/packages/evolution/src/VrfVkey.ts b/packages/evolution/src/VrfVkey.ts index ffe9b5e1..6edf7a27 100644 --- a/packages/evolution/src/VrfVkey.ts +++ b/packages/evolution/src/VrfVkey.ts @@ -1,7 +1,6 @@ -import { Data, FastCheck, pipe, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, Schema } from "effect" import * as Bytes32 from "./Bytes32.js" -import { createEncoders } from "./Codec.js" /** * Error class for VrfVkey related operations. @@ -22,7 +21,7 @@ export class VrfVkeyError extends Data.TaggedError("VrfVkeyError")<{ * @since 2.0.0 * @category schemas */ -export const VrfVkey = pipe(Bytes32.HexSchema, Schema.brand("VrfVkey")).annotations({ +export const VrfVkey = Bytes32.HexSchema.pipe(Schema.brand("VrfVkey")).annotations({ identifier: "VrfVkey" }) @@ -51,26 +50,140 @@ export const FromHex = Schema.compose( export const equals = (a: VrfVkey, b: VrfVkey): boolean => a === b /** - * Generate a random VrfVkey. + * Check if the given value is a valid VrfVkey * * @since 2.0.0 - * @category generators + * @category predicates */ -export const generator = FastCheck.uint8Array({ - minLength: Bytes32.Bytes32_BYTES_LENGTH, - maxLength: Bytes32.Bytes32_BYTES_LENGTH -}).map((bytes) => Codec.Decode.bytes(bytes)) +export const isVrfVkey = Schema.is(VrfVkey) /** - * Codec utilities for VrfVkey encoding and decoding operations. + * FastCheck arbitrary for generating random VrfVkey instances. * * @since 2.0.0 - * @category encoding/decoding + * @category arbitrary */ -export const Codec = createEncoders( - { - bytes: FromBytes, - hex: FromHex - }, - VrfVkeyError -) +export const arbitrary = FastCheck.hexaString({ + minLength: Bytes32.HEX_LENGTH, + maxLength: Bytes32.HEX_LENGTH +}).map((hex) => hex as VrfVkey) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse VrfVkey from bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromBytes = (bytes: Uint8Array): VrfVkey => + Eff.runSync(Effect.fromBytes(bytes)) + +/** + * Parse VrfVkey from hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromHex = (hex: string): VrfVkey => + Eff.runSync(Effect.fromHex(hex)) + +/** + * Encode VrfVkey to bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toBytes = (vrfVkey: VrfVkey): Uint8Array => + Eff.runSync(Effect.toBytes(vrfVkey)) + +/** + * Encode VrfVkey to hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toHex = (vrfVkey: VrfVkey): string => + Eff.runSync(Effect.toHex(vrfVkey)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse VrfVkey from bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromBytes = (bytes: Uint8Array): Eff.Effect => + Schema.decode(FromBytes)(bytes).pipe( + Eff.mapError( + (cause) => + new VrfVkeyError({ + message: "Failed to parse VrfVkey from bytes", + cause + }) + ) + ) + + /** + * Parse VrfVkey from hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromHex = (hex: string): Eff.Effect => + Schema.decode(FromHex)(hex).pipe( + Eff.mapError( + (cause) => + new VrfVkeyError({ + message: "Failed to parse VrfVkey from hex", + cause + }) + ) + ) + + /** + * Encode VrfVkey to bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toBytes = (vrfVkey: VrfVkey): Eff.Effect => + Schema.encode(FromBytes)(vrfVkey).pipe( + Eff.mapError( + (cause) => + new VrfVkeyError({ + message: "Failed to encode VrfVkey to bytes", + cause + }) + ) + ) + + /** + * Encode VrfVkey to hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toHex = (vrfVkey: VrfVkey): Eff.Effect => + Schema.encode(FromHex)(vrfVkey).pipe( + Eff.mapError( + (cause) => + new VrfVkeyError({ + message: "Failed to encode VrfVkey to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/Withdrawals.ts b/packages/evolution/src/Withdrawals.ts index 8576c1a9..a3743941 100644 --- a/packages/evolution/src/Withdrawals.ts +++ b/packages/evolution/src/Withdrawals.ts @@ -1,4 +1,4 @@ -import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" @@ -19,7 +19,7 @@ import * as RewardAccount from "./RewardAccount.js" * @category errors */ export class WithdrawalsError extends Data.TaggedError("WithdrawalsError")<{ - message: string + message?: string cause?: unknown }> {} @@ -31,22 +31,14 @@ export class WithdrawalsError extends Data.TaggedError("WithdrawalsError")<{ * ``` * * @since 2.0.0 - * @since 2.0.0 * @category model */ export class Withdrawals extends Schema.TaggedClass()("Withdrawals", { withdrawals: Schema.MapFromSelf({ key: RewardAccount.RewardAccount, - value: Coin.CoinSchema + value: Coin.Coin }) -}) { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { - _tag: "Withdrawals", - withdrawals: Array.from(this.withdrawals.entries()).map(([acc, coin]) => [acc, coin.toString()]) - } - } -} +}) {} /** * Check if the given value is a valid Withdrawals @@ -56,6 +48,11 @@ export class Withdrawals extends Schema.TaggedClass()("Withdrawals" */ export const isWithdrawals = Schema.is(Withdrawals) +export const CDDLSchema = Schema.MapFromSelf({ + key: Schema.Uint8ArrayFromSelf, // RewardAccount as Uint8Array (29 bytes) + value: CBOR.Integer // Coin as bigint +}) + /** * CDDL schema for Withdrawals. * @@ -66,38 +63,31 @@ export const isWithdrawals = Schema.is(Withdrawals) * @since 2.0.0 * @category schemas */ -export const WithdrawalsCDDLSchema = Schema.transformOrFail( - Schema.MapFromSelf({ - key: Schema.Uint8ArrayFromSelf, // RewardAccount as Uint8Array (29 bytes) - value: CBOR.Integer // Coin as bigint - }), - Schema.typeSchema(Withdrawals), - { - strict: true, - encode: (toA) => - Effect.gen(function* () { - const withdrawalsMap = new Map() - for (const [rewardAccount, coin] of toA.withdrawals.entries()) { - const accountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(rewardAccount) - withdrawalsMap.set(accountBytes, BigInt(coin)) - } - return withdrawalsMap - }), - decode: (fromA) => - Effect.gen(function* () { - const decodedWithdrawals = new Map() - for (const [accountBytes, coinAmount] of fromA.entries()) { - const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(accountBytes) - const coin = Coin.make(coinAmount) - decodedWithdrawals.set(rewardAccount, coin) - } - return yield* ParseResult.decode(Withdrawals)({ - _tag: "Withdrawals", - withdrawals: decodedWithdrawals - }) +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Withdrawals), { + strict: true, + encode: (toA) => + Eff.gen(function* () { + const withdrawalsMap = new Map() + for (const [rewardAccount, coin] of toA.withdrawals.entries()) { + const accountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(rewardAccount) + withdrawalsMap.set(accountBytes, BigInt(coin)) + } + return withdrawalsMap + }), + decode: (fromA) => + Eff.gen(function* () { + const decodedWithdrawals = new Map() + for (const [accountBytes, coinAmount] of fromA.entries()) { + const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(accountBytes) + const coin = Coin.make(coinAmount) + decodedWithdrawals.set(rewardAccount, coin) + } + return yield* ParseResult.decode(Withdrawals)({ + _tag: "Withdrawals", + withdrawals: decodedWithdrawals }) - } -) + }) +}) /** * CBOR bytes transformation schema for Withdrawals. @@ -105,10 +95,10 @@ export const WithdrawalsCDDLSchema = Schema.transformOrFail( * @since 2.0.0 * @category schemas */ -export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - WithdrawalsCDDLSchema // CBOR → Withdrawals + FromCDDL // CBOR → Withdrawals ) /** @@ -117,10 +107,10 @@ export const FromBytes = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => * @since 2.0.0 * @category schemas */ -export const FromHex = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( Bytes.FromHex, // string → Uint8Array - FromBytes(options) // Uint8Array → Withdrawals + FromCBORBytes(options) // Uint8Array → Withdrawals ) /** @@ -141,12 +131,12 @@ export const equals = (self: Withdrawals, that: Withdrawals): boolean => { } /** - * FastCheck generator for Withdrawals instances. + * FastCheck arbitrary for Withdrawals instances. * * @since 2.0.0 - * @category generators + * @category testing */ -export const generator = FastCheck.array(FastCheck.tuple(RewardAccount.generator, Coin.generator), { +export const arbitrary = FastCheck.array(FastCheck.tuple(RewardAccount.arbitrary, Coin.arbitrary), { minLength: 0, maxLength: 10 }).map((entries) => new Withdrawals({ withdrawals: new Map(entries) })) @@ -248,29 +238,138 @@ export const size = (withdrawals: Withdrawals): number => withdrawals.withdrawal export const entries = (withdrawals: Withdrawals): Array<[RewardAccount.RewardAccount, Coin.Coin]> => Array.from(withdrawals.withdrawals.entries()) -export const Codec = (options: CBOR.CodecOptions = CBOR.DEFAULT_OPTIONS) => ({ - Encode: { - cborBytes: Schema.encodeSync(FromBytes(options)), - cborHex: Schema.encodeSync(FromHex(options)) - }, - Decode: { - cborBytes: Schema.decodeUnknownSync(FromBytes(options)), - cborHex: Schema.decodeUnknownSync(FromHex(options)) - }, - EncodeEither: { - cborBytes: Schema.encodeEither(FromBytes(options)), - cborHex: Schema.encodeEither(FromHex(options)) - }, - DecodeEither: { - cborBytes: Schema.decodeUnknownEither(FromBytes(options)), - cborHex: Schema.decodeUnknownEither(FromHex(options)) - }, - EncodeEffect: { - cborBytes: Schema.encode(FromBytes(options)), - cborHex: Schema.encode(FromHex(options)) - }, - DecodeEffect: { - cborBytes: Schema.decodeUnknown(FromBytes(options)), - cborHex: Schema.decodeUnknown(FromHex(options)) - } -}) +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse a Withdrawals from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Withdrawals => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse a Withdrawals from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Withdrawals => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert a Withdrawals to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (withdrawals: Withdrawals, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(withdrawals, options)) + +/** + * Convert a Withdrawals to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (withdrawals: Withdrawals, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(withdrawals, options)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse a Withdrawals from CBOR bytes. + * + * @since 2.0.0 + * @category effect + */ + export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (error) => + new WithdrawalsError({ + message: "Failed to decode Withdrawals from CBOR bytes", + cause: error + }) + ) + ) + + /** + * Parse a Withdrawals from CBOR hex string. + * + * @since 2.0.0 + * @category effect + */ + export const fromCBORHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (error) => + new WithdrawalsError({ + message: "Failed to decode Withdrawals from CBOR hex", + cause: error + }) + ) + ) + + /** + * Convert a Withdrawals to CBOR bytes. + * + * @since 2.0.0 + * @category effect + */ + export const toCBORBytes = ( + withdrawals: Withdrawals, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORBytes(options))(withdrawals).pipe( + Eff.mapError( + (error) => + new WithdrawalsError({ + message: "Failed to encode Withdrawals to CBOR bytes", + cause: error + }) + ) + ) + + /** + * Convert a Withdrawals to CBOR hex string. + * + * @since 2.0.0 + * @category effect + */ + export const toCBORHex = ( + withdrawals: Withdrawals, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORHex(options))(withdrawals).pipe( + Eff.mapError( + (error) => + new WithdrawalsError({ + message: "Failed to encode Withdrawals to CBOR hex", + cause: error + }) + ) + ) +} diff --git a/packages/evolution/src/index.ts b/packages/evolution/src/index.ts index 541e3088..401ce04b 100644 --- a/packages/evolution/src/index.ts +++ b/packages/evolution/src/index.ts @@ -7,6 +7,8 @@ export * as AuxiliaryDataHash from "./AuxiliaryDataHash.js" export * as BaseAddress from "./BaseAddress.js" export * as Bech32 from "./Bech32.js" export * as BigInt from "./BigInt.js" +export * as Bip32PrivateKey from "./Bip32PrivateKey.js" +export * as Bip32PublicKey from "./Bip32PublicKey.js" export * as Block from "./Block.js" export * as BlockBodyHash from "./BlockBodyHash.js" export * as BlockHeaderHash from "./BlockHeaderHash.js" @@ -20,6 +22,8 @@ export * as Bytes32 from "./Bytes32.js" export * as Bytes57 from "./Bytes57.js" export * as Bytes64 from "./Bytes64.js" export * as Bytes80 from "./Bytes80.js" +export * as Bytes96 from "./Bytes96.js" +export * as Bytes128 from "./Bytes128.js" export * as Bytes448 from "./Bytes448.js" export * as CBOR from "./CBOR.js" export * as Certificate from "./Certificate.js" @@ -41,6 +45,7 @@ export * as Ed25519Signature from "./Ed25519Signature.js" export * as EnterpriseAddress from "./EnterpriseAddress.js" export * as EpochNo from "./EpochNo.js" export * as FormatError from "./FormatError.js" +export * as GovernanceAction from "./GovernanceAction.js" export * as Hash28 from "./Hash28.js" export * as Header from "./Header.js" export * as HeaderBody from "./HeaderBody.js" @@ -69,6 +74,7 @@ export * as PoolMetadata from "./PoolMetadata.js" export * as PoolParams from "./PoolParams.js" export * as Port from "./Port.js" export * as PositiveCoin from "./PositiveCoin.js" +export * as PrivateKey from "./PrivateKey.js" export * as ProposalProcedures from "./ProposalProcedures.js" export * as ProtocolVersion from "./ProtocolVersion.js" export * as Relay from "./Relay.js" diff --git a/packages/evolution/test/Address.test.ts b/packages/evolution/test/Address.test.ts index d4e2840b..421e313d 100644 --- a/packages/evolution/test/Address.test.ts +++ b/packages/evolution/test/Address.test.ts @@ -82,7 +82,7 @@ describe("Address", () => { expect(() => { Address.Codec.Decode.bech32(INVALID_ADDRESS) }).toThrow() - + expect(() => { Address.Codec.Decode.bech32("") }).toThrow() @@ -103,11 +103,11 @@ describe("Address", () => { expect(() => { Address.Codec.Decode.hex("not-a-hex-address") }).toThrow() - + expect(() => { Address.Codec.Decode.hex("123xyz") }).toThrow() - + expect(() => { Address.Codec.Decode.hex("") }).toThrow() @@ -141,16 +141,17 @@ describe("Address", () => { }) it("should convert between bech32 and hex formats", () => { - for (const address of ALL_ADDRESSES.slice(0, 2)) { // Test a couple of addresses + for (const address of ALL_ADDRESSES.slice(0, 2)) { + // Test a couple of addresses try { // bech32 -> bytes -> hex const addr = Address.Codec.Decode.bech32(address) const hex = Address.Codec.Encode.hex(addr) - + // hex -> bytes -> bech32 const addrFromHex = Address.Codec.Decode.hex(hex) const backToBech32 = Address.Codec.Encode.bech32(addrFromHex) - + // Should match the original expect(backToBech32.toLowerCase()).toBe(address.toLowerCase()) } catch (error) { @@ -189,7 +190,7 @@ describe("Address", () => { // Bech32 addresses are case-insensitive const lowerCaseAddr = MAINNET_ADDRESSES[0].toLowerCase() const upperCaseAddr = MAINNET_ADDRESSES[0].toUpperCase() - + try { const addr1 = Address.Codec.Decode.bech32(lowerCaseAddr) const addr2 = Address.Codec.Decode.bech32(upperCaseAddr) @@ -206,15 +207,15 @@ describe("Address", () => { // Base address (mainnet) const baseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) expect(baseAddr._tag).toBe("BaseAddress") - + // Enterprise address (mainnet) const enterpriseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[6]) expect(enterpriseAddr._tag).toBe("EnterpriseAddress") - + // Reward address (mainnet) const rewardAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[8]) expect(rewardAddr._tag).toBe("RewardAccount") - + // Pointer address (testnet) const pointerAddr = Address.Codec.Decode.bech32(TESTNET_ADDRESSES[4]) expect(pointerAddr._tag).toBe("PointerAddress") @@ -222,7 +223,7 @@ describe("Address", () => { expect.fail(`Failed to decode address: ${error}`) } }) - + it("should correctly identify network IDs", () => { try { // Mainnet address @@ -232,7 +233,7 @@ describe("Address", () => { } else { expect.fail("Failed to decode as BaseAddress") } - + // Testnet address const testnetAddr = Address.Codec.Decode.bech32(TESTNET_ADDRESSES[0]) if (testnetAddr._tag === "BaseAddress") { @@ -247,7 +248,7 @@ describe("Address", () => { it("should correctly extract payment credential from base address", () => { try { - const baseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) + const baseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) if (baseAddr._tag === "BaseAddress") { expect(baseAddr.paymentCredential).toBeDefined() } else { @@ -278,13 +279,13 @@ describe("Address", () => { Address.Codec.Decode.bech32(INVALID_ADDRESS) }).toThrow() }) - + it("should throw errors for invalid hex addresses", () => { expect(() => { Address.Codec.Decode.hex("invalid-hex") }).toThrow() }) - + it("should throw errors for empty addresses", () => { expect(() => { Address.Codec.Decode.bech32("") @@ -296,46 +297,46 @@ describe("Address", () => { it("should generate valid addresses", () => { // Get a sample address from the generator const generatedAddr = FastCheck.sample(Address.generator, 1)[0] - + // Check that we can encode it without errors const bytes = Address.Codec.Encode.bytes(generatedAddr) expect(bytes).toBeDefined() - + // Verify the address can be encoded to bech32 const bech32 = Address.Codec.Encode.bech32(generatedAddr) expect(bech32).toBeDefined() expect(bech32.length).toBeGreaterThan(0) - + // Verify the address can be encoded to hex const hex = Address.Codec.Encode.hex(generatedAddr) expect(hex).toBeDefined() expect(hex.length).toBeGreaterThan(0) }) }) - + describe("Address additional features", () => { it("should handle direct encoding between formats", () => { // First decode a bech32 address to get an Address object const addr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) - + // Encode to bytes const bytes = Address.Codec.Encode.bytes(addr) expect(bytes).toBeDefined() - + // Encode to hex const hex = Address.Codec.Encode.hex(addr) expect(hex).toBeDefined() - + // Encode back to bech32 const bech32 = Address.Codec.Encode.bech32(addr) expect(bech32).toBeDefined() - + // The full conversion cycle should preserve the address const addrFromHex = Address.Codec.Decode.hex(hex) const bech32FromHex = Address.Codec.Encode.bech32(addrFromHex) expect(bech32FromHex.toLowerCase()).toBe(MAINNET_ADDRESSES[0].toLowerCase()) }) - + it("should correctly identify address properties", () => { // Process a Base Address (type 0) const baseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) @@ -346,7 +347,7 @@ describe("Address", () => { } else { expect.fail(`Expected BaseAddress but got ${baseAddr._tag}`) } - + // Process an Enterprise Address (type 6) const enterpriseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[6]) if (enterpriseAddr._tag === "EnterpriseAddress") { @@ -355,7 +356,7 @@ describe("Address", () => { } else { expect.fail(`Expected EnterpriseAddress but got ${enterpriseAddr._tag}`) } - + // Process a Pointer Address (type 4) const pointerAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[4]) if (pointerAddr._tag === "PointerAddress") { @@ -365,7 +366,7 @@ describe("Address", () => { } else { expect.fail(`Expected PointerAddress but got ${pointerAddr._tag}`) } - + // Process a Reward Account (type 14) const rewardAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[8]) if (rewardAddr._tag === "RewardAccount") { @@ -375,7 +376,7 @@ describe("Address", () => { expect.fail(`Expected RewardAccount but got ${rewardAddr._tag}`) } }) - + it("should validate address network consistency", () => { // Test mainnet addresses for (const address of MAINNET_ADDRESSES) { @@ -384,7 +385,7 @@ describe("Address", () => { expect(addr.networkId).toBe(1) } } - + // Test testnet addresses for (const address of TESTNET_ADDRESSES) { const addr = Address.Codec.Decode.bech32(address) @@ -395,4 +396,3 @@ describe("Address", () => { }) }) }) - diff --git a/packages/evolution/test/Bip32PrivateKey.CML.test.ts b/packages/evolution/test/Bip32PrivateKey.CML.test.ts new file mode 100644 index 00000000..7ba85054 --- /dev/null +++ b/packages/evolution/test/Bip32PrivateKey.CML.test.ts @@ -0,0 +1,411 @@ +import * as CML from "@dcspark/cardano-multiplatform-lib-nodejs" +import { mnemonicToEntropy } from "bip39" +import { beforeEach, describe, expect, it } from "vitest" + +import { Bip32PrivateKey, Bip32PublicKey, PrivateKey } from "../src/index.js" + +/** + * Comprehensive CML compatibility tests for Bip32PrivateKey module. + * Tests all major operations against CML reference implementation. + */ +describe("Bip32PrivateKey CML Compatibility", () => { + // Test data + const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + const testPassword = "" + const testEntropy = Buffer.from(mnemonicToEntropy(testMnemonic), "hex") + + // Helper function to harden indices (same as reference) + function harden(num: number): number { + if (typeof num !== "number") throw new Error("Type number required here!") + return 0x80000000 + num + } + + describe("Master Key Generation", () => { + it("should generate identical master keys from BIP39 entropy", () => { + // CML approach + const cmlRootKey = CML.Bip32PrivateKey.from_bip39_entropy( + new Uint8Array(testEntropy), + testPassword ? new TextEncoder().encode(testPassword) : new Uint8Array() + ) + + // Evolution SDK approach + const evolutionRootKey = Bip32PrivateKey.fromBip39Entropy(new Uint8Array(testEntropy), testPassword) + + // Compare raw bytes + const cmlRootBytes = cmlRootKey.to_raw_bytes() + const evolutionRootBytes = Bip32PrivateKey.toBytes(evolutionRootKey) + + expect(Buffer.from(cmlRootBytes)).toEqual(Buffer.from(evolutionRootBytes)) + }) + + it("should generate identical master keys with password", () => { + const password = "test-password-123" + + // CML approach + const cmlRootKey = CML.Bip32PrivateKey.from_bip39_entropy( + new Uint8Array(testEntropy), + new TextEncoder().encode(password) + ) + + // Evolution SDK approach + const evolutionRootKey = Bip32PrivateKey.fromBip39Entropy(new Uint8Array(testEntropy), password) + + // Compare raw bytes + const cmlRootBytes = cmlRootKey.to_raw_bytes() + const evolutionRootBytes = Bip32PrivateKey.toBytes(evolutionRootKey) + + expect(Buffer.from(cmlRootBytes)).toEqual(Buffer.from(evolutionRootBytes)) + }) + }) + + describe("Key Derivation", () => { + let cmlRootKey: CML.Bip32PrivateKey + let evolutionRootKey: Bip32PrivateKey.Bip32PrivateKey + + beforeEach(() => { + cmlRootKey = CML.Bip32PrivateKey.from_bip39_entropy(new Uint8Array(testEntropy), new Uint8Array()) + evolutionRootKey = Bip32PrivateKey.fromBip39Entropy(new Uint8Array(testEntropy), testPassword) + }) + + it("should derive identical hardened child keys", () => { + const index = harden(1852) + + // CML derivation + const cmlChild = cmlRootKey.derive(index) + + // Evolution SDK derivation + const evolutionChild = Bip32PrivateKey.derive(evolutionRootKey, [index]) + + // Compare raw bytes + const cmlChildBytes = cmlChild.to_raw_bytes() + const evolutionChildBytes = Bip32PrivateKey.toBytes(evolutionChild) + + expect(Buffer.from(cmlChildBytes)).toEqual(Buffer.from(evolutionChildBytes)) + }) + + it("should derive identical soft child keys", () => { + const index = 0 // Soft derivation + + // CML derivation + const cmlChild = cmlRootKey.derive(index) + + // Evolution SDK derivation + const evolutionChild = Bip32PrivateKey.derive(evolutionRootKey, [index]) + + // Compare raw bytes + const cmlChildBytes = cmlChild.to_raw_bytes() + const evolutionChildBytes = Bip32PrivateKey.toBytes(evolutionChild) + + expect(Buffer.from(cmlChildBytes)).toEqual(Buffer.from(evolutionChildBytes)) + }) + + it("should derive identical Cardano account keys (m/1852'/1815'/0')", () => { + // CML approach + const cmlAccountKey = cmlRootKey + .derive(harden(1852)) // purpose + .derive(harden(1815)) // coin_type + .derive(harden(0)) // account + + // Evolution SDK approach + const evolutionAccountKey = Bip32PrivateKey.derive(evolutionRootKey, [harden(1852), harden(1815), harden(0)]) + + // Compare raw bytes + const cmlAccountBytes = cmlAccountKey.to_raw_bytes() + const evolutionAccountBytes = Bip32PrivateKey.toBytes(evolutionAccountKey) + + expect(Buffer.from(cmlAccountBytes)).toEqual(Buffer.from(evolutionAccountBytes)) + }) + + it("should derive identical payment keys (account/0/0)", () => { + // Derive account key first + const cmlAccountKey = cmlRootKey.derive(harden(1852)).derive(harden(1815)).derive(harden(0)) + + const evolutionAccountKey = Bip32PrivateKey.derive(evolutionRootKey, [harden(1852), harden(1815), harden(0)]) + + // Derive payment keys + const cmlPaymentKey = cmlAccountKey.derive(0).derive(0) // role=0, index=0 + const evolutionPaymentKey = Bip32PrivateKey.derive(evolutionAccountKey, [0, 0]) // role=0, index=0 + + // Compare raw bytes + const cmlPaymentBytes = cmlPaymentKey.to_raw_bytes() + const evolutionPaymentBytes = Bip32PrivateKey.toBytes(evolutionPaymentKey) + + expect(Buffer.from(cmlPaymentBytes)).toEqual(Buffer.from(evolutionPaymentBytes)) + }) + + it("should derive identical stake keys (account/2/0)", () => { + // Derive account key first + const cmlAccountKey = cmlRootKey.derive(harden(1852)).derive(harden(1815)).derive(harden(0)) + + const evolutionAccountKey = Bip32PrivateKey.derive(evolutionRootKey, [harden(1852), harden(1815), harden(0)]) + + // Derive stake keys + const cmlStakeKey = cmlAccountKey.derive(2).derive(0) // role=2, index=0 + const evolutionStakeKey = Bip32PrivateKey.derive(evolutionAccountKey, [2, 0]) // role=2, index=0 + + // Compare raw bytes + const cmlStakeBytes = cmlStakeKey.to_raw_bytes() + const evolutionStakeBytes = Bip32PrivateKey.toBytes(evolutionStakeKey) + + expect(Buffer.from(cmlStakeBytes)).toEqual(Buffer.from(evolutionStakeBytes)) + }) + + it("should derive using multiple indices (array)", () => { + const indices = [harden(1852), harden(1815), harden(0), 0, 0] + + // CML approach (step by step) + let cmlKey = cmlRootKey + for (const index of indices) { + cmlKey = cmlKey.derive(index) + } + + // Evolution SDK approach (using derive method) + const evolutionKey = Bip32PrivateKey.derive(evolutionRootKey, indices) + + // Compare raw bytes + const cmlKeyBytes = cmlKey.to_raw_bytes() + const evolutionKeyBytes = Bip32PrivateKey.toBytes(evolutionKey) + + expect(Buffer.from(cmlKeyBytes)).toEqual(Buffer.from(evolutionKeyBytes)) + }) + + it("should derive using path string", () => { + const path = "m/1852'/1815'/0'/0/0" + const indices = [harden(1852), harden(1815), harden(0), 0, 0] + + // CML approach (step by step) + let cmlKey = cmlRootKey + for (const index of indices) { + cmlKey = cmlKey.derive(index) + } + + // Evolution SDK approach (using derivePath method) + const evolutionKey = Bip32PrivateKey.derivePath(evolutionRootKey, path) + + // Compare raw bytes + const cmlKeyBytes = cmlKey.to_raw_bytes() + const evolutionKeyBytes = Bip32PrivateKey.toBytes(evolutionKey) + + expect(Buffer.from(cmlKeyBytes)).toEqual(Buffer.from(evolutionKeyBytes)) + }) + }) + + describe("Public Key Derivation", () => { + let cmlRootKey: any + let evolutionRootKey: Bip32PrivateKey.Bip32PrivateKey + + beforeEach(() => { + cmlRootKey = CML.Bip32PrivateKey.from_bip39_entropy(new Uint8Array(testEntropy), new Uint8Array()) + evolutionRootKey = Bip32PrivateKey.fromBip39Entropy(new Uint8Array(testEntropy), testPassword) + }) + + it("should derive identical public keys from private keys", () => { + // Derive payment key + const cmlAccountKey = cmlRootKey.derive(harden(1852)).derive(harden(1815)).derive(harden(0)) + const cmlPaymentKey = cmlAccountKey.derive(0).derive(0) + + const evolutionAccountKey = Bip32PrivateKey.derive(evolutionRootKey, [harden(1852), harden(1815), harden(0)]) + const evolutionPaymentKey = Bip32PrivateKey.derive(evolutionAccountKey, [0, 0]) + + // Get raw private keys for comparison + const cmlPaymentRaw = cmlPaymentKey.to_raw_key() + const evolutionPaymentPrivateKey = Bip32PrivateKey.toPrivateKey(evolutionPaymentKey) + + // Compare raw private key bytes + const cmlPaymentRawBytes = cmlPaymentRaw.to_raw_bytes() + const evolutionPaymentRawBytes = PrivateKey.toBytes(evolutionPaymentPrivateKey) + + expect(Buffer.from(cmlPaymentRawBytes)).toEqual(Buffer.from(evolutionPaymentRawBytes)) + + // Get public keys + const cmlPublicKey = cmlPaymentRaw.to_public() + const evolutionPublicKey = Bip32PrivateKey.toPublicKey(evolutionPaymentKey) + + // Compare public key bytes (raw 32-byte key part) + const cmlPublicBytes = cmlPublicKey.to_raw_bytes() + const evolutionPublicBytes = Bip32PublicKey.toRawBytes(evolutionPublicKey) + + expect(Buffer.from(cmlPublicBytes)).toEqual(Buffer.from(evolutionPublicBytes)) + }) + + it("should support public key derivation path matching", () => { + // Create account keys + const cmlAccountKey = cmlRootKey.derive(harden(1852)).derive(harden(1815)).derive(harden(0)) + + const evolutionAccountKey = Bip32PrivateKey.derive(evolutionRootKey, [harden(1852), harden(1815), harden(0)]) + + // Derive payment key via private key derivation + const cmlPaymentKey = cmlAccountKey.derive(0).derive(0) + const evolutionPaymentKey = Bip32PrivateKey.derive(evolutionAccountKey, [0, 0]) + + // Get public keys from private keys + const cmlPaymentRaw = cmlPaymentKey.to_raw_key() + const cmlPublicKey = cmlPaymentRaw.to_public() + const evolutionPublicKey = Bip32PrivateKey.toPublicKey(evolutionPaymentKey) + + // Compare public key bytes + const cmlPublicBytes = cmlPublicKey.to_raw_bytes() + const evolutionPublicBytes = Bip32PublicKey.toRawBytes(evolutionPublicKey) + + expect(Buffer.from(cmlPublicBytes)).toEqual(Buffer.from(evolutionPublicBytes)) + + // Test public key derivation path (if we can derive the same keys using public key derivation) + const evolutionAccountPubKey = Bip32PrivateKey.toPublicKey(evolutionAccountKey) + const evolutionAccountKeyBytes = Bip32PrivateKey.toBytes(evolutionAccountKey) + const evolutionAccountChainCode = evolutionAccountKeyBytes.slice(64, 96) + + // Get the raw bytes from the account public key + const evolutionAccountPubKeyRaw = Bip32PublicKey.toRawBytes(evolutionAccountPubKey) + + const evolutionAccountBip32Pub = Bip32PublicKey.fromBytes(evolutionAccountPubKeyRaw, evolutionAccountChainCode) + + // Derive payment key via public key derivation + const paymentRoleKey = Bip32PublicKey.deriveChild(evolutionAccountBip32Pub, 0) + const evolutionPaymentViaPub = Bip32PublicKey.deriveChild(paymentRoleKey, 0) + + const evolutionPaymentViaPubKey = Bip32PublicKey.toRawBytes(evolutionPaymentViaPub) + + // Both derivation methods should produce the same public key + expect(Buffer.from(evolutionPublicBytes)).toEqual(Buffer.from(evolutionPaymentViaPubKey)) + }) + }) + + describe("CML Compatibility Format", () => { + let evolutionRootKey: Bip32PrivateKey.Bip32PrivateKey + + beforeEach(() => { + evolutionRootKey = Bip32PrivateKey.fromBip39Entropy(new Uint8Array(testEntropy), testPassword) + }) + + it("should convert to and from CML 128-byte format", () => { + // Convert to CML format + const cml128Bytes = Bip32PrivateKey.to128XPRV(evolutionRootKey) + expect(cml128Bytes.length).toBe(128) + + // Convert back from CML format + const restoredKey = Bip32PrivateKey.from128XPRV(cml128Bytes) + + // Should be identical to original + const originalBytes = Bip32PrivateKey.toBytes(evolutionRootKey) + const restoredBytes = Bip32PrivateKey.toBytes(restoredKey) + + expect(Buffer.from(originalBytes)).toEqual(Buffer.from(restoredBytes)) + }) + + it("should be compatible with CML's 128-byte format structure", () => { + // Get CML key for reference + const cmlKey = CML.Bip32PrivateKey.from_bip39_entropy(new Uint8Array(testEntropy), new Uint8Array()) + const cml128Bytes = cmlKey.to_128_xprv() + + // Convert Evolution key to 128-byte format + const evolution128Bytes = Bip32PrivateKey.to128XPRV(evolutionRootKey) + + // The formats should have the same structure + expect(evolution128Bytes.length).toBe(cml128Bytes.length) + expect(evolution128Bytes.length).toBe(128) + + // They should be identical since we start from the same entropy + expect(Buffer.from(evolution128Bytes)).toEqual(Buffer.from(cml128Bytes)) + }) + + it("should roundtrip through CML format without data loss", () => { + // Original key + const originalBytes = Bip32PrivateKey.toBytes(evolutionRootKey) + + // Convert to CML format and back + const cml128Bytes = Bip32PrivateKey.to128XPRV(evolutionRootKey) + const restoredKey = Bip32PrivateKey.from128XPRV(cml128Bytes) + const restoredBytes = Bip32PrivateKey.toBytes(restoredKey) + + // Should be identical + expect(Buffer.from(originalBytes)).toEqual(Buffer.from(restoredBytes)) + + // Derived keys should also be identical + const originalDerived = Bip32PrivateKey.deriveChild(evolutionRootKey, harden(1852)) + const restoredDerived = Bip32PrivateKey.deriveChild(restoredKey, harden(1852)) + + const originalDerivedBytes = Bip32PrivateKey.toBytes(originalDerived) + const restoredDerivedBytes = Bip32PrivateKey.toBytes(restoredDerived) + + expect(Buffer.from(originalDerivedBytes)).toEqual(Buffer.from(restoredDerivedBytes)) + }) + }) + + describe("Edge Cases and Error Handling", () => { + it("should handle maximum hardened index", () => { + const maxHardened = 0xffffffff + const evolutionRootKey = Bip32PrivateKey.fromBip39Entropy(new Uint8Array(testEntropy), testPassword) + + // Should not throw for maximum hardened index + expect(() => { + Bip32PrivateKey.deriveChild(evolutionRootKey, maxHardened) + }).not.toThrow() + }) + + it("should handle derivation of many levels", () => { + const evolutionRootKey = Bip32PrivateKey.fromBip39Entropy(new Uint8Array(testEntropy), testPassword) + const deepPath = [harden(1852), harden(1815), harden(0), 0, 0, 1, 2, 3, 4, 5] + + // Should not throw for deep derivation + expect(() => { + Bip32PrivateKey.derive(evolutionRootKey, deepPath) + }).not.toThrow() + }) + + it("should handle different entropy sizes", () => { + const entropy128 = new Uint8Array(16) // 128 bits + const entropy256 = new Uint8Array(32) // 256 bits + + // Fill with test data + entropy128.fill(0xaa) + entropy256.fill(0xbb) + + expect(() => { + Bip32PrivateKey.fromBip39Entropy(entropy128, "") + }).not.toThrow() + + expect(() => { + Bip32PrivateKey.fromBip39Entropy(entropy256, "") + }).not.toThrow() + }) + }) + + describe("Cardano Path Utilities", () => { + it("should generate correct Cardano BIP44 indices", () => { + const paymentIndices = Bip32PrivateKey.CardanoPath.paymentIndices(0, 0) + const stakeIndices = Bip32PrivateKey.CardanoPath.stakeIndices(0, 0) + + expect(paymentIndices).toEqual([ + harden(1852), // Purpose + harden(1815), // Coin type (ADA) + harden(0), // Account + 0, // Role (payment) + 0 // Index + ]) + + expect(stakeIndices).toEqual([ + harden(1852), // Purpose + harden(1815), // Coin type (ADA) + harden(0), // Account + 2, // Role (stake) + 0 // Index + ]) + }) + + it("should derive keys using Cardano path utilities", () => { + const evolutionRootKey = Bip32PrivateKey.fromBip39Entropy(new Uint8Array(testEntropy), testPassword) + + // Derive using utility + const paymentKey = Bip32PrivateKey.derive(evolutionRootKey, Bip32PrivateKey.CardanoPath.paymentIndices(0, 0)) + const stakeKey = Bip32PrivateKey.derive(evolutionRootKey, Bip32PrivateKey.CardanoPath.stakeIndices(0, 0)) + + // Derive manually for comparison + const manual = Bip32PrivateKey.derive(evolutionRootKey, [harden(1852), harden(1815), harden(0), 0, 0]) + + const manualStake = Bip32PrivateKey.derive(evolutionRootKey, [harden(1852), harden(1815), harden(0), 2, 0]) + + expect(Buffer.from(Bip32PrivateKey.toBytes(paymentKey))).toEqual(Buffer.from(Bip32PrivateKey.toBytes(manual))) + expect(Buffer.from(Bip32PrivateKey.toBytes(stakeKey))).toEqual(Buffer.from(Bip32PrivateKey.toBytes(manualStake))) + }) + }) +}) diff --git a/packages/evolution/test/Data.golden.test.ts b/packages/evolution/test/Data.golden.test.ts index d1550931..0c641c88 100644 --- a/packages/evolution/test/Data.golden.test.ts +++ b/packages/evolution/test/Data.golden.test.ts @@ -204,10 +204,6 @@ const getTestCases = ( return entries.slice(0, count) } -/** - * Initialize the codec for CBOR encoding/decoding operations - */ -const Codec = Data.Codec() /** * Golden Tests for Data module @@ -222,7 +218,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid integer sample at index ${entry.index}`) } const plutusData = Data.int(entry.sample.integer) - const encoded = Codec.Encode.cborHex(plutusData) + const encoded = Data.toCBORHex(plutusData) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -235,7 +231,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid integer sample at index ${entry.index}`) } const plutusData = Data.int(entry.sample.integer) - const encoded = Codec.Encode.cborBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -244,7 +240,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("integer", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborHex(entry.cborHex) + const decoded = Data.fromCBORHex(entry.cborHex) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -253,7 +249,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("integer", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborBytes(new Uint8Array(entry.cborBytes)) + const decoded = Data.fromCBORBytes(new Uint8Array(entry.cborBytes)) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -266,8 +262,8 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid integer sample at index ${entry.index}`) } const plutusData = Data.int(entry.sample.integer) - const encoded = Codec.Encode.cborHex(plutusData) - const decoded = Codec.Decode.cborHex(encoded) + const encoded = Data.toCBORHex(plutusData) + const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) @@ -283,7 +279,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid byte array sample at index ${entry.index}`) } const plutusData = Data.bytearray(entry.sample.bytearray) - const encoded = Codec.Encode.cborHex(plutusData) + const encoded = Data.toCBORHex(plutusData) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -296,7 +292,7 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid byte array sample at index ${entry.index}`) } const plutusData = Data.bytearray(entry.sample.bytearray) - const encoded = Codec.Encode.cborBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -305,7 +301,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("byteArray", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborHex(entry.cborHex) + const decoded = Data.fromCBORHex(entry.cborHex) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -314,7 +310,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("byteArray", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborBytes(new Uint8Array(entry.cborBytes)) + const decoded = Data.fromCBORBytes(new Uint8Array(entry.cborBytes)) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -327,8 +323,8 @@ describe("Data Golden Tests", () => { throw new Error(`Invalid byte array sample at index ${entry.index}`) } const plutusData = Data.bytearray(entry.sample.bytearray) - const encoded = Codec.Encode.cborHex(plutusData) - const decoded = Codec.Decode.cborHex(encoded) + const encoded = Data.toCBORHex(plutusData) + const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) @@ -345,7 +341,7 @@ describe("Data Golden Tests", () => { } const plutusDataList = entry.sample.list.map(reconstructPlutusData) const plutusData = Data.list(plutusDataList) - const encoded = Codec.Encode.cborHex(plutusData) + const encoded = Data.toCBORHex(plutusData) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }, 30000) // 30 second timeout for large test cases @@ -359,7 +355,7 @@ describe("Data Golden Tests", () => { } const plutusDataList = entry.sample.list.map(reconstructPlutusData) const plutusData = Data.list(plutusDataList) - const encoded = Codec.Encode.cborBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -368,7 +364,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("list", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborHex(entry.cborHex) + const decoded = Data.fromCBORHex(entry.cborHex) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -377,7 +373,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("list", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborBytes(new Uint8Array(entry.cborBytes)) + const decoded = Data.fromCBORBytes(new Uint8Array(entry.cborBytes)) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -391,8 +387,8 @@ describe("Data Golden Tests", () => { } const plutusDataList = entry.sample.list.map(reconstructPlutusData) const plutusData = Data.list(plutusDataList) - const encoded = Codec.Encode.cborHex(plutusData) - const decoded = Codec.Decode.cborHex(encoded) + const encoded = Data.toCBORHex(plutusData) + const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) @@ -407,12 +403,12 @@ describe("Data Golden Tests", () => { if (!isMapSample(entry.sample)) { throw new Error(`Invalid map sample at index ${entry.index}`) } - const plutusDataEntries = entry.sample.entries.map((entryObj: { key: unknown; value: unknown }) => ({ - key: reconstructPlutusData(entryObj.key), - value: reconstructPlutusData(entryObj.value) - })) + const plutusDataEntries = entry.sample.entries.map((entryObj: { key: unknown; value: unknown }) => [ + reconstructPlutusData(entryObj.key), + reconstructPlutusData(entryObj.value) + ] as [Data.Data, Data.Data]) const plutusData = Data.map(plutusDataEntries) - const encoded = Codec.Encode.cborHex(plutusData) + const encoded = Data.toCBORHex(plutusData) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -424,12 +420,12 @@ describe("Data Golden Tests", () => { if (!isMapSample(entry.sample)) { throw new Error(`Invalid map sample at index ${entry.index}`) } - const plutusDataEntries = entry.sample.entries.map((entryObj: { key: unknown; value: unknown }) => ({ - key: reconstructPlutusData(entryObj.key), - value: reconstructPlutusData(entryObj.value) - })) + const plutusDataEntries = entry.sample.entries.map((entryObj: { key: unknown; value: unknown }) => [ + reconstructPlutusData(entryObj.key), + reconstructPlutusData(entryObj.value) + ] as [Data.Data, Data.Data]) const plutusData = Data.map(plutusDataEntries) - const encoded = Codec.Encode.cborBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -438,7 +434,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("map", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborHex(entry.cborHex) + const decoded = Data.fromCBORHex(entry.cborHex) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -447,7 +443,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("map", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborBytes(new Uint8Array(entry.cborBytes)) + const decoded = Data.fromCBORBytes(new Uint8Array(entry.cborBytes)) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -459,13 +455,13 @@ describe("Data Golden Tests", () => { if (!isMapSample(entry.sample)) { throw new Error(`Invalid map sample at index ${entry.index}`) } - const plutusDataEntries = entry.sample.entries.map((entryObj: { key: unknown; value: unknown }) => ({ - key: reconstructPlutusData(entryObj.key), - value: reconstructPlutusData(entryObj.value) - })) + const plutusDataEntries = entry.sample.entries.map((entryObj: { key: unknown; value: unknown }) => [ + reconstructPlutusData(entryObj.key), + reconstructPlutusData(entryObj.value) + ] as [Data.Data, Data.Data]) const plutusData = Data.map(plutusDataEntries) - const encoded = Codec.Encode.cborHex(plutusData) - const decoded = Codec.Decode.cborHex(encoded) + const encoded = Data.toCBORHex(plutusData) + const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) @@ -482,7 +478,7 @@ describe("Data Golden Tests", () => { } const plutusDataFields = entry.sample.fields.map(reconstructPlutusData) const plutusData = Data.constr(BigInt(entry.sample.index), plutusDataFields) - const encoded = Codec.Encode.cborHex(plutusData) + const encoded = Data.toCBORHex(plutusData) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -496,7 +492,7 @@ describe("Data Golden Tests", () => { } const plutusDataFields = entry.sample.fields.map(reconstructPlutusData) const plutusData = Data.constr(BigInt(entry.sample.index), plutusDataFields) - const encoded = Codec.Encode.cborBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -505,7 +501,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("constr", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborHex(entry.cborHex) + const decoded = Data.fromCBORHex(entry.cborHex) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -514,7 +510,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("constr", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborBytes(new Uint8Array(entry.cborBytes)) + const decoded = Data.fromCBORBytes(new Uint8Array(entry.cborBytes)) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -528,8 +524,8 @@ describe("Data Golden Tests", () => { } const plutusDataFields = entry.sample.fields.map(reconstructPlutusData) const plutusData = Data.constr(BigInt(entry.sample.index), plutusDataFields) - const encoded = Codec.Encode.cborHex(plutusData) - const decoded = Codec.Decode.cborHex(encoded) + const encoded = Data.toCBORHex(plutusData) + const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) @@ -542,7 +538,7 @@ describe("Data Golden Tests", () => { testCases.forEach((entry) => { const plutusData = reconstructPlutusData(entry.sample) - const encoded = Codec.Encode.cborHex(plutusData) + const encoded = Data.toCBORHex(plutusData) expect(encoded, `Failed at sample index ${entry.index}`).toBe(entry.cborHex) }) }) @@ -552,7 +548,7 @@ describe("Data Golden Tests", () => { testCases.forEach((entry) => { const plutusData = reconstructPlutusData(entry.sample) - const encoded = Codec.Encode.cborBytes(plutusData) + const encoded = Data.toCBORBytes(plutusData) expect(Array.from(encoded), `Failed at sample index ${entry.index}`).toEqual(entry.cborBytes) }) }) @@ -561,7 +557,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("data", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborHex(entry.cborHex) + const decoded = Data.fromCBORHex(entry.cborHex) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -570,7 +566,7 @@ describe("Data Golden Tests", () => { const testCases = getTestCases("data", "decoding") testCases.forEach((entry) => { - const decoded = Codec.Decode.cborBytes(new Uint8Array(entry.cborBytes)) + const decoded = Data.fromCBORBytes(new Uint8Array(entry.cborBytes)) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) }) @@ -580,8 +576,8 @@ describe("Data Golden Tests", () => { testCases.forEach((entry) => { const plutusData = reconstructPlutusData(entry.sample) - const encoded = Codec.Encode.cborHex(plutusData) - const decoded = Codec.Decode.cborHex(encoded) + const encoded = Data.toCBORHex(plutusData) + const decoded = Data.fromCBORHex(encoded) expect(normalizeDecodedData(decoded), `Failed at sample index ${entry.index}`).toEqual(entry.sample) }) @@ -610,10 +606,10 @@ const reconstructPlutusData = (sample: unknown): Data.Data => { return Data.list((typedSample.list as Array).map(reconstructPlutusData)) case "Map": return Data.map( - (typedSample.entries as Array<{ key: unknown; value: unknown }>).map((entry) => ({ - key: reconstructPlutusData(entry.key), - value: reconstructPlutusData(entry.value) - })) + (typedSample.entries as Array<{ key: unknown; value: unknown }>).map((entry) => [ + reconstructPlutusData(entry.key), + reconstructPlutusData(entry.value) + ]) ) case "Constr": return Data.constr( diff --git a/packages/evolution/test/Data.test.ts b/packages/evolution/test/Data.test.ts index 7af57acc..6d28f59b 100644 --- a/packages/evolution/test/Data.test.ts +++ b/packages/evolution/test/Data.test.ts @@ -82,32 +82,21 @@ describe("Data Module Tests", () => { describe("Plutus Map", () => { it("should create a valid empty map", () => { const map = Data.map([]) - expect((map as Data.MapList).size).toBe(0) + expect((map as Data.Map).size).toBe(0) expect(Data.isMap(map)).toBe(true) }) it("should create a valid map with entries", () => { const map = Data.map([ - { - key: "cafe", - value: 42n - }, - { - key: 99n, - value: "deadbeef" - } + ["cafe", 42n], + [99n, "deadbeef"] ]) - expect((map as Data.MapList).size).toBe(2) + expect((map as Data.Map).size).toBe(2) expect(Data.isMap(map)).toBe(true) }) it("should validate Plutus Map with schema", () => { - const map = Data.map([ - { - key: 1n, - value: 2n - } - ]) + const map = Data.map([[1n, 2n]]) expect(Data.isMap(map)).toBe(true) }) }) @@ -175,12 +164,7 @@ describe("Data Module Tests", () => { }, { name: "map with entries", - value: Data.map([ - { - key: 1n, - value: "cafe" - } - ]), + value: Data.map([[1n, "cafe"]]), expectedHex: "bf0142cafeff" }, { @@ -322,14 +306,8 @@ describe("Data Module Tests", () => { const complex = Data.constr(0n, [ [1n, 2n, "cafe"], Data.map([ - { - key: 42n, - value: ["deadbeef"] - }, - { - key: "deadbeef", - value: Data.constr(1n, [-999n]) - } + [42n, ["deadbeef"]], + ["deadbeef", Data.constr(1n, [-999n])] ]), Data.constr(7n, [[], Data.map([])]) ]) @@ -481,16 +459,16 @@ describe("Data Module Tests", () => { it("should handle large maps", () => { // Create a map with many entries - const entries = Array.from({ length: 100 }, (_, i) => ({ - key: BigInt(i), - value: `${i.toString(16).padStart(4, "0")}` - })) + const entries = Array.from( + { length: 100 }, + (_, i) => [BigInt(i), `${i.toString(16).padStart(4, "0")}`] as [Data.Int, Data.ByteArray] + ) const largeMap = Data.map(entries) const encoded = Codec.Encode.cborBytes(largeMap) const decoded = Codec.Decode.cborBytes(encoded) expect(decoded).toEqual(largeMap) - expect((decoded as Data.MapList).size).toBe(100) + expect((decoded as Data.Map).size).toBe(100) }) it("should handle constructors with many fields", () => { @@ -642,7 +620,7 @@ describe("Data Module Tests", () => { expect(Data.isInt(42n)).toBe(true) expect(Data.isBytes("deadbeef")).toBe(true) expect(Data.isList([1n])).toBe(true) - expect(Data.isMap(Data.map([{ key: 1n, value: 2n }]))).toBe(true) + expect(Data.isMap(Data.map([[1n, 2n]]))).toBe(true) expect(Data.isConstr(Data.constr(0n, [42n]))).toBe(true) }) }) @@ -667,16 +645,13 @@ describe("Data Module Tests", () => { it("should handle mixed nested structures", () => { // Create a complex mixed structure with valid hex strings const complex = Data.constr(0n, [ - [Data.map([{ key: 1n, value: "cafe" }]), Data.constr(1n, [-999n])], + [Data.map([[1n, "cafe"]]), Data.constr(1n, [-999n])], Data.map([ - { - key: "deadbeef", // Valid hex string - value: [1n, 2n, 3n] - }, - { - key: 42n, - value: Data.constr(2n, []) - } + [ + "deadbeef", // Valid hex string + [1n, 2n, 3n] + ], + [42n, Data.constr(2n, [])] ]) ]) @@ -794,30 +769,13 @@ describe("Data Module Tests", () => { it("should handle unsorted map and return sorted canonical format", () => { const unsorted = Data.constr(15n, [ Data.map([ - { - key: 9358323691080620716n, - value: 6877988357227539948n - }, - { - key: "70f3d8f8803183c15033", - value: "88472079a8ce" - }, - { - key: "0f7f249c0a9f4f829d2e4f", - value: 13859100696864903302n - }, - { - key: "07", - value: "b649" - } + [9358323691080620716n, 6877988357227539948n], + ["70f3d8f8803183c15033", "88472079a8ce"], + ["0f7f249c0a9f4f829d2e4f", 13859100696864903302n], + ["07", "b649"] ]), 6n, - Data.map([ - { - key: "65b7f7", - value: 5432168958238743917n - } - ]) + Data.map([["65b7f7", 5432168958238743917n]]) ]) const encoded = CodecCanonical.Encode.cborHex(unsorted) diff --git a/packages/evolution/test/PrivateKey.CML.test.ts b/packages/evolution/test/PrivateKey.CML.test.ts new file mode 100644 index 00000000..b7669f0f --- /dev/null +++ b/packages/evolution/test/PrivateKey.CML.test.ts @@ -0,0 +1,424 @@ +import * as CML from "@dcspark/cardano-multiplatform-lib-nodejs" +import { describe, expect, it } from "vitest" + +import * as PrivateKey from "../src/PrivateKey" +import * as VKey from "../src/VKey" + +// Test compatibility with CML (Cardano Multiplatform Library) +describe("PrivateKey CML Compatibility", () => { + // Sample test data - using valid 32-byte and 64-byte keys + const sampleBytes32 = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 + ]) + + const sampleBytes64 = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, + 0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 + ]) + + const testMessage = new Uint8Array([0x01, 0x02, 0x03, 0x04]) + + describe("Missing Functions Implementation", () => { + it("should have generate method", () => { + const privateKeyBytes = PrivateKey.generate() + expect(privateKeyBytes).toBeDefined() + expect(privateKeyBytes.length).toBe(32) // Raw bytes for 32-byte key + + // Convert to PrivateKey to verify it works + const privateKey = PrivateKey.fromBytes(privateKeyBytes) + const bytes = PrivateKey.toBytes(privateKey) + expect(bytes.length).toBe(32) + }) + + it("should have generateExtended method", () => { + const privateKeyBytes = PrivateKey.generateExtended() + expect(privateKeyBytes).toBeDefined() + expect(privateKeyBytes.length).toBe(64) // Raw bytes for 64-byte key + + // Convert to PrivateKey to verify it works + const privateKey = PrivateKey.fromBytes(privateKeyBytes) + const bytes = PrivateKey.toBytes(privateKey) + expect(bytes.length).toBe(64) + }) + + it("should have toPublic method", () => { + const privateKey = PrivateKey.fromBytes(sampleBytes32) + expect(PrivateKey.toBytes(privateKey).length).toBe(32) // Input should be 32 bytes + + const publicKey = PrivateKey.toPublicKey(privateKey) + expect(publicKey).toBeDefined() + + const publicKeyBytes = VKey.toBytes(publicKey) + expect(publicKeyBytes.length).toBe(32) // Public key should be 32 bytes + }) + }) + + describe("Constructor Methods Compatibility", () => { + it("should match CML.PrivateKey.from_normal_bytes", () => { + expect(sampleBytes32.length).toBe(32) // Verify input is 32 bytes + + const cmlPrivateKey = CML.PrivateKey.from_normal_bytes(sampleBytes32) + expect(cmlPrivateKey.to_raw_bytes().length).toBe(32) // CML should return 32 bytes + + const evolutionPrivateKey = PrivateKey.fromBytes(sampleBytes32) + + // Compare the raw bytes - convert evolution hex string to bytes + const evolutionBytes = PrivateKey.toBytes(evolutionPrivateKey) + expect(evolutionBytes.length).toBe(32) // Evolution should also be 32 bytes + expect(Buffer.from(cmlPrivateKey.to_raw_bytes())).toEqual(Buffer.from(evolutionBytes)) + }) + + it("should match CML.PrivateKey.from_extended_bytes", () => { + expect(sampleBytes64.length).toBe(64) // Verify input is 64 bytes + + const cmlPrivateKey = CML.PrivateKey.from_extended_bytes(sampleBytes64) + expect(cmlPrivateKey.to_raw_bytes().length).toBe(64) // CML should return 64 bytes + + const evolutionPrivateKey = PrivateKey.fromBytes(sampleBytes64) + + // Compare the raw bytes - convert evolution hex string to bytes + const evolutionBytes = PrivateKey.toBytes(evolutionPrivateKey) + expect(evolutionBytes.length).toBe(64) // Evolution should also be 64 bytes + expect(Buffer.from(cmlPrivateKey.to_raw_bytes())).toEqual(Buffer.from(evolutionBytes)) + }) + }) + + describe("Output Methods Compatibility", () => { + it("should generate CML-compatible bytes output", () => { + expect(sampleBytes32.length).toBe(32) // Verify input is 32 bytes + + const cmlPrivateKey = CML.PrivateKey.from_normal_bytes(sampleBytes32) + expect(cmlPrivateKey.to_raw_bytes().length).toBe(32) // CML output should be 32 bytes + + const evolutionPrivateKey = PrivateKey.fromBytes(sampleBytes32) + + // Raw bytes should match - convert evolution hex string to bytes + const evolutionBytes = PrivateKey.toBytes(evolutionPrivateKey) + expect(evolutionBytes.length).toBe(32) // Evolution output should be 32 bytes + expect(Buffer.from(cmlPrivateKey.to_raw_bytes())).toEqual(Buffer.from(evolutionBytes)) + }) + + it("should generate CML-compatible bech32 output", () => { + expect(sampleBytes32.length).toBe(32) // Verify input is 32 bytes + + const cmlPrivateKey = CML.PrivateKey.from_normal_bytes(sampleBytes32) + expect(cmlPrivateKey.to_raw_bytes().length).toBe(32) // CML should maintain 32 bytes + + const evolutionPrivateKey = PrivateKey.fromBytes(sampleBytes32) + + // Note: CML and Evolution use different bech32 prefixes: + // CML: "ed25519_sk" vs Evolution: "ed25519e_sk" + // This is a known difference and doesn't affect functionality. + // Let's verify the underlying data is the same by comparing the original bytes + const cmlBech32 = cmlPrivateKey.to_bech32() + const evolutionBech32 = PrivateKey.toBech32(evolutionPrivateKey) + + // Both should be valid bech32 strings + expect(cmlBech32).toMatch(/^ed25519[e]?_sk1[a-z0-9]+$/) + expect(evolutionBech32).toMatch(/^ed25519[e]?_sk1[a-z0-9]+$/) + + // The original bytes should be identical + const cmlBytes = cmlPrivateKey.to_raw_bytes() + expect(cmlBytes.length).toBe(32) // CML bytes should be 32 + + const evolutionBytes = PrivateKey.toBytes(evolutionPrivateKey) + expect(evolutionBytes.length).toBe(32) // Evolution bytes should be 32 + expect(Buffer.from(cmlBytes)).toEqual(Buffer.from(evolutionBytes)) + + // Evolution should be able to decode its own bech32 + const evolutionFromBech32 = PrivateKey.fromBech32(evolutionBech32) + const evolutionDecodedBytes = PrivateKey.toBytes(evolutionFromBech32) + expect(Buffer.from(evolutionDecodedBytes)).toEqual(Buffer.from(evolutionBytes)) + }) + + it("should generate CML-compatible hex output", () => { + expect(sampleBytes32.length).toBe(32) // Verify input is 32 bytes + + const cmlPrivateKey = CML.PrivateKey.from_normal_bytes(sampleBytes32) + expect(cmlPrivateKey.to_raw_bytes().length).toBe(32) // CML should maintain 32 bytes + + const evolutionPrivateKey = PrivateKey.fromBytes(sampleBytes32) + + // CML doesn't have to_hex method, so compare with raw bytes converted to hex + const cmlBytes = cmlPrivateKey.to_raw_bytes() + expect(cmlBytes.length).toBe(32) // Verify CML bytes are 32 + + const cmlHex = Array.from(cmlBytes as Uint8Array) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + expect(cmlHex.length).toBe(64) // Hex string should be 64 chars (32 bytes * 2) + expect(cmlHex).toEqual(PrivateKey.toHex(evolutionPrivateKey)) + }) + }) + + describe("Generator Methods Compatibility", () => { + it("should have generate method like CML.PrivateKey.generate_ed25519", () => { + // Test that our generate method produces valid keys + const evolutionPrivateKeyBytes = PrivateKey.generate() + expect(evolutionPrivateKeyBytes.length).toBe(32) // Normal key is 32 bytes + + // Should be able to create a CML key from our generated bytes + const cmlPrivateKey = CML.PrivateKey.from_normal_bytes(evolutionPrivateKeyBytes) + expect(cmlPrivateKey).toBeDefined() + expect(cmlPrivateKey.to_raw_bytes().length).toBe(32) // CML should also maintain 32 bytes + }) + + it("should have generateExtended method like CML.PrivateKey.generate_ed25519extended", () => { + // Test that our generateExtended method produces valid keys + const evolutionPrivateKeyBytes = PrivateKey.generateExtended() + expect(evolutionPrivateKeyBytes.length).toBe(64) // Extended key is 64 bytes + + // Should be able to create a CML key from our generated bytes + const cmlPrivateKey = CML.PrivateKey.from_extended_bytes(evolutionPrivateKeyBytes) + expect(cmlPrivateKey).toBeDefined() + expect(cmlPrivateKey.to_raw_bytes().length).toBe(64) // CML should also maintain 64 bytes + + // Compare with CML's own generation + const cmlGenerated = CML.PrivateKey.generate_ed25519extended() + const cmlGeneratedBytes = cmlGenerated.to_raw_bytes() + expect(cmlGeneratedBytes.length).toBe(64) // Should also be 64 bytes + }) + }) + + describe("Cryptographic Operations Compatibility", () => { + it("should generate identical signatures for 32-byte keys", () => { + expect(sampleBytes32.length).toBe(32) // Verify input is 32 bytes + expect(testMessage.length).toBe(4) // Verify test message length + + // Create identical keys + const cmlPrivateKey = CML.PrivateKey.from_normal_bytes(sampleBytes32) + expect(cmlPrivateKey.to_raw_bytes().length).toBe(32) // CML key should be 32 bytes + + const evolutionPrivateKey = PrivateKey.fromBytes(sampleBytes32) + expect(PrivateKey.toBytes(evolutionPrivateKey).length).toBe(32) // Evolution key should be 32 bytes + + // Sign the same message + const cmlSignature = cmlPrivateKey.sign(testMessage) + expect(cmlSignature.to_raw_bytes().length).toBe(64) // Ed25519 signature is 64 bytes + + const evolutionSignature = PrivateKey.sign(evolutionPrivateKey, testMessage) + expect(evolutionSignature.length).toBe(64) // Evolution signature should also be 64 bytes + + // Signatures should be identical + expect(Buffer.from(cmlSignature.to_raw_bytes())).toEqual(Buffer.from(evolutionSignature)) + + // Verify signatures are cryptographically valid + const cmlPublicKey = cmlPrivateKey.to_public() + const evolutionPublicKey = PrivateKey.toPublicKey(evolutionPrivateKey) + + // CML signature verification + const cmlVerifyResult = cmlPublicKey.verify(testMessage, cmlSignature) + expect(cmlVerifyResult).toBe(true) + + // Evolution signature verification using VKey + const evolutionVerifyResult = VKey.verify(evolutionPublicKey, testMessage, evolutionSignature) + expect(evolutionVerifyResult).toBe(true) + + // Cross-verify: CML signature with Evolution public key + const crossVerifyEvolution = VKey.verify(evolutionPublicKey, testMessage, cmlSignature.to_raw_bytes()) + expect(crossVerifyEvolution).toBe(true) + + // Cross-verify: Evolution signature with CML public key + const evolutionSignatureForCml = CML.Ed25519Signature.from_raw_bytes(evolutionSignature) + const crossVerifyCml = cmlPublicKey.verify(testMessage, evolutionSignatureForCml) + expect(crossVerifyCml).toBe(true) + }) + + it("should generate identical signatures for 64-byte extended keys", () => { + expect(sampleBytes64.length).toBe(64) // Verify input is 64 bytes + expect(testMessage.length).toBe(4) // Verify test message length + + // Create identical extended keys + const cmlPrivateKey = CML.PrivateKey.from_extended_bytes(sampleBytes64) + expect(cmlPrivateKey.to_raw_bytes().length).toBe(64) // CML extended key should be 64 bytes + + const evolutionPrivateKey = PrivateKey.fromBytes(sampleBytes64) + expect(PrivateKey.toBytes(evolutionPrivateKey).length).toBe(64) // Evolution extended key should be 64 bytes + + // Sign the same message + const cmlSignature = cmlPrivateKey.sign(testMessage) + expect(cmlSignature.to_raw_bytes().length).toBe(64) // Ed25519 signature is 64 bytes + + const evolutionSignature = PrivateKey.sign(evolutionPrivateKey, testMessage) + expect(evolutionSignature.length).toBe(64) // Evolution signature should also be 64 bytes + + // Signatures should be identical + expect(Buffer.from(cmlSignature.to_raw_bytes())).toEqual(Buffer.from(evolutionSignature)) + + // Verify signatures are cryptographically valid + const cmlPublicKey = cmlPrivateKey.to_public() + const evolutionPublicKey = PrivateKey.toPublicKey(evolutionPrivateKey) + + // CML signature verification + const cmlVerifyResult = cmlPublicKey.verify(testMessage, cmlSignature) + expect(cmlVerifyResult).toBe(true) + + // Evolution signature verification using VKey + const evolutionVerifyResult = VKey.verify(evolutionPublicKey, testMessage, evolutionSignature) + expect(evolutionVerifyResult).toBe(true) + + // Cross-verify: CML signature with Evolution public key + const crossVerifyEvolution = VKey.verify(evolutionPublicKey, testMessage, cmlSignature.to_raw_bytes()) + expect(crossVerifyEvolution).toBe(true) + + // Cross-verify: Evolution signature with CML public key + const evolutionSignatureForCml = CML.Ed25519Signature.from_raw_bytes(evolutionSignature) + const crossVerifyCml = cmlPublicKey.verify(testMessage, evolutionSignatureForCml) + expect(crossVerifyCml).toBe(true) + }) + }) + + describe("Public Key Derivation Compatibility", () => { + it("should derive identical public keys from normal private keys", () => { + expect(sampleBytes32.length).toBe(32) // Verify input is 32 bytes + + // Create identical keys + const cmlPrivateKey = CML.PrivateKey.from_normal_bytes(sampleBytes32) + expect(cmlPrivateKey.to_raw_bytes().length).toBe(32) // CML private key should be 32 bytes + + const evolutionPrivateKey = PrivateKey.fromBytes(sampleBytes32) + expect(PrivateKey.toBytes(evolutionPrivateKey).length).toBe(32) // Evolution private key should be 32 bytes + + // Compare public keys + const cmlPublicKey = cmlPrivateKey.to_public() + const evolutionPublicKey = PrivateKey.toPublicKey(evolutionPrivateKey) + + const cmlPublicBytes = cmlPublicKey.to_raw_bytes() + expect(cmlPublicBytes.length).toBe(32) // Ed25519 public key is 32 bytes + + const evolutionPublicBytes = VKey.toBytes(evolutionPublicKey) + expect(evolutionPublicBytes.length).toBe(32) // Evolution public key should also be 32 bytes + + expect(Buffer.from(cmlPublicBytes)).toEqual(Buffer.from(evolutionPublicBytes)) + }) + + it("should derive identical public keys from extended private keys", () => { + expect(sampleBytes64.length).toBe(64) // Verify input is 64 bytes + + // Create identical extended keys + const cmlExtended = CML.PrivateKey.from_extended_bytes(sampleBytes64) + expect(cmlExtended.to_raw_bytes().length).toBe(64) // CML extended private key should be 64 bytes + + const evolutionExtended = PrivateKey.fromBytes(sampleBytes64) + expect(PrivateKey.toBytes(evolutionExtended).length).toBe(64) // Evolution extended private key should be 64 bytes + + // Compare public keys derived from extended keys + const cmlPublicKey = cmlExtended.to_public() + const evolutionPublicKey = PrivateKey.toPublicKey(evolutionExtended) + + const cmlPublicBytes = cmlPublicKey.to_raw_bytes() + expect(cmlPublicBytes.length).toBe(32) // Public key should still be 32 bytes + + const evolutionPublicBytes = VKey.toBytes(evolutionPublicKey) + expect(evolutionPublicBytes.length).toBe(32) // Evolution public key should also be 32 bytes + + expect(Buffer.from(cmlPublicBytes)).toEqual(Buffer.from(evolutionPublicBytes)) + }) + }) + + describe("Roundtrip Compatibility", () => { + it("should handle CML-generated keys", () => { + // Generate a key with CML + const cmlPrivateKey = CML.PrivateKey.generate_ed25519() + const cmlBytes = cmlPrivateKey.to_raw_bytes() + expect(cmlBytes.length).toBe(32) // CML normal key should be 32 bytes + + // Import it into our system + const evolutionPrivateKey = PrivateKey.fromBytes(cmlBytes) + const evolutionBytes = PrivateKey.toBytes(evolutionPrivateKey) + expect(evolutionBytes.length).toBe(32) // Evolution should maintain 32 bytes + + // Should produce identical results + expect(Buffer.from(evolutionBytes)).toEqual(Buffer.from(cmlBytes)) + + // Should produce identical signatures + const message = new Uint8Array([1, 2, 3, 4]) + expect(message.length).toBe(4) // Verify message length + + const cmlSignature = cmlPrivateKey.sign(message) + expect(cmlSignature.to_raw_bytes().length).toBe(64) // Signature should be 64 bytes + + const evolutionSignature = PrivateKey.sign(evolutionPrivateKey, message) + expect(evolutionSignature.length).toBe(64) // Evolution signature should also be 64 bytes + + expect(Buffer.from(cmlSignature.to_raw_bytes())).toEqual(Buffer.from(evolutionSignature)) + + // Verify signatures are cryptographically valid + const cmlPublicKey = cmlPrivateKey.to_public() + const evolutionPublicKey = PrivateKey.toPublicKey(evolutionPrivateKey) + + // Both signatures should verify correctly + const cmlVerifyResult = cmlPublicKey.verify(message, cmlSignature) + expect(cmlVerifyResult).toBe(true) + + const evolutionVerifyResult = VKey.verify(evolutionPublicKey, message, evolutionSignature) + expect(evolutionVerifyResult).toBe(true) + + // Cross-verification should also work + const crossVerifyEvolution = VKey.verify(evolutionPublicKey, message, cmlSignature.to_raw_bytes()) + expect(crossVerifyEvolution).toBe(true) + + const evolutionSigForCml = CML.Ed25519Signature.from_raw_bytes(evolutionSignature) + const crossVerifyCml = cmlPublicKey.verify(message, evolutionSigForCml) + expect(crossVerifyCml).toBe(true) + }) + + it("should handle CML-generated extended keys", () => { + // Generate an extended key with CML + const cmlPrivateKey = CML.PrivateKey.generate_ed25519extended() + const cmlBytes = cmlPrivateKey.to_raw_bytes() + expect(cmlBytes.length).toBe(64) // CML extended key should be 64 bytes + + // Import it into our system + const evolutionPrivateKey = PrivateKey.fromBytes(cmlBytes) + + // Should produce identical results + const evolutionBytes = PrivateKey.toBytes(evolutionPrivateKey) + expect(evolutionBytes.length).toBe(64) // Evolution should maintain 64 bytes + expect(Buffer.from(evolutionBytes)).toEqual(Buffer.from(cmlBytes)) + + // For signatures, both should produce valid signatures, but they might be different + // due to different extended key handling. Let's verify they're both valid. + const message = new Uint8Array([1, 2, 3, 4]) + expect(message.length).toBe(4) // Verify message length + + const cmlSignature = cmlPrivateKey.sign(message) + const evolutionSignature = PrivateKey.sign(evolutionPrivateKey, message) + + // Both signatures should be 64 bytes (Ed25519 signature length) + expect(cmlSignature.to_raw_bytes().length).toBe(64) + expect(evolutionSignature.length).toBe(64) + + // For extended keys, the signatures might be different due to different derivation + // So let's just verify both are valid by checking they can verify + const cmlPublicKey = cmlPrivateKey.to_public() + const evolutionPublicKey = PrivateKey.toPublicKey(evolutionPrivateKey) + + // Both should derive the same public key + const cmlPublicBytes = cmlPublicKey.to_raw_bytes() + expect(cmlPublicBytes.length).toBe(32) // Public key should be 32 bytes + + const evolutionPublicBytes = VKey.toBytes(evolutionPublicKey) + expect(evolutionPublicBytes.length).toBe(32) // Evolution public key should also be 32 bytes + expect(Buffer.from(cmlPublicBytes)).toEqual(Buffer.from(evolutionPublicBytes)) + + // Verify both signatures are cryptographically valid + const cmlVerifyResult = cmlPublicKey.verify(message, cmlSignature) + expect(cmlVerifyResult).toBe(true) + + const evolutionVerifyResult = VKey.verify(evolutionPublicKey, message, evolutionSignature) + expect(evolutionVerifyResult).toBe(true) + + // Since public keys are identical, cross-verification should work too + const crossVerifyEvolution = VKey.verify(evolutionPublicKey, message, cmlSignature.to_raw_bytes()) + expect(crossVerifyEvolution).toBe(true) + + const evolutionSigForCml = CML.Ed25519Signature.from_raw_bytes(evolutionSignature) + const crossVerifyCml = cmlPublicKey.verify(message, evolutionSigForCml) + expect(crossVerifyCml).toBe(true) + }) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3fab15e..4ebbd8fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: rimraf: specifier: ^6.0.0 version: 6.0.1 + tsx: + specifier: ^4.20.3 + version: 4.20.3 turbo: specifier: ^2.0.0 version: 2.5.5 @@ -123,25 +126,46 @@ importers: packages/evolution: dependencies: - '@effect/platform': - specifier: ^0.90.0 - version: 0.90.0(effect@3.17.3) '@effect/platform-node': - specifier: ^0.94.0 - version: 0.94.0(@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) + specifier: ^0.94.1 + version: 0.94.1(@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) + '@noble/hashes': + specifier: ^1.6.0 + version: 1.8.0 '@scure/base': - specifier: ^1.1.0 + specifier: ^1.2.0 version: 1.2.6 + '@scure/bip32': + specifier: ^1.5.0 + version: 1.7.0 + '@scure/bip39': + specifier: ^1.4.0 + version: 1.6.0 + '@types/bip39': + specifier: ^3.0.4 + version: 3.0.4 + bip39: + specifier: ^3.1.0 + version: 3.1.0 dockerode: specifier: ^4.0.0 version: 4.0.7 effect: specifier: ^3.17.3 version: 3.17.3 + libsodium-wrappers-sumo: + specifier: ^0.7.15 + version: 0.7.15 devDependencies: + '@dcspark/cardano-multiplatform-lib-nodejs': + specifier: ^6.2.0 + version: 6.2.0 '@types/dockerode': specifier: ^3.3.0 version: 3.3.42 + '@types/libsodium-wrappers-sumo': + specifier: ^0.7.8 + version: 0.7.8 tsx: specifier: ^4.20.3 version: 4.20.3 @@ -312,6 +336,9 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dcspark/cardano-multiplatform-lib-nodejs@6.2.0': + resolution: {integrity: sha512-D4swEw9rkMySB3uCZlZgTKBZwfc/lkvPNLJI9ZYfUzXCstA8Vk2e77I4WbSw4Vkyt1qgEUDTs9+DT6hMGBlMhw==} + '@dependents/detective-less@5.0.1': resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} @@ -342,11 +369,11 @@ packages: '@effect/eslint-plugin@0.3.2': resolution: {integrity: sha512-c4Vs9t3r54A4Zpl+wo8+PGzZz3JWYsip41H+UrebRLjQ2Hk/ap63IeCgN/HWcYtxtyhRopjp7gW9nOQ2Snbl+g==} - '@effect/experimental@0.54.2': - resolution: {integrity: sha512-edHopTBPXQdgyuR4ytRmyoPq7Ubt6CS4TymiXpyd6lu3kxlK7K78/eTMju9hRfJbiSDFVy8S5C2dW8CH9i66Rg==} + '@effect/experimental@0.54.3': + resolution: {integrity: sha512-FR+4KfGxte/BwQyVvbq8boWSWyN5p69tdtUQX9Owf/JfnLmZY42d+L3nnn1Gg8EhTPiAk+hMnODWWHnV03JmbQ==} peerDependencies: '@effect/platform': ^0.90.0 - effect: ^3.17.3 + effect: ^3.17.4 ioredis: ^5 lmdb: ^3 peerDependenciesMeta: @@ -369,25 +396,25 @@ packages: '@effect/sql': ^0.44.0 effect: ^3.17.1 - '@effect/platform-node@0.94.0': - resolution: {integrity: sha512-nLiv/h2qTrELPo/QOdgf3D41627Zct4ffmU+3MpLgbe8bjqPGEtBh489qNvVwg24+g4EjVbEosCv39cN+w1tQQ==} + '@effect/platform-node@0.94.1': + resolution: {integrity: sha512-dxIPPCpBH8AIlzDftcmmNHxoOX1iLi7H+iS5Dy8JT/ftoao41iTv/MrvP3M0ZqoNhJQ6ZbLTPZYNiDBDLGf1DA==} peerDependencies: - '@effect/cluster': ^0.46.0 + '@effect/cluster': ^0.46.2 '@effect/platform': ^0.90.0 - '@effect/rpc': ^0.68.0 + '@effect/rpc': ^0.68.2 '@effect/sql': ^0.44.0 - effect: ^3.17.1 + effect: ^3.17.6 '@effect/platform@0.90.0': resolution: {integrity: sha512-F26RZO8qVyCLH43EF9BvJwrhtFsZL2Xv66Jxxjj/sBIes8TOVpyebaysQ7Tz33xALobwU1eNgm8vh18VkJiWnQ==} peerDependencies: effect: ^3.17.0 - '@effect/rpc@0.68.0': - resolution: {integrity: sha512-1fFLE5MgSpu+kTy+A2SrPVNTnAIPWlLIlSeslIE9R22EvR1eCkskWxcBDvKNyfqCefXNhSRY5UafG3XpTYumaw==} + '@effect/rpc@0.68.2': + resolution: {integrity: sha512-AFmOeB+Tl71yIDCA9ZSK0wd2uWZrPTvkJ4kcGo8Ad7okUMsAwwz2AOfJHayFbbA4XRUS8rLmSYI8H3oM0yvqVQ==} peerDependencies: '@effect/platform': ^0.90.0 - effect: ^3.17.1 + effect: ^3.17.5 '@effect/sql@0.44.0': resolution: {integrity: sha512-HxVEk9ufZZnJ2AuqUlgirjlSDYQ49QDM6o7MkcFQtp4UKrCmDgTshNre11rACOiMZH3ywH6cWViJ1eLwf10D2A==} @@ -1031,6 +1058,14 @@ packages: '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} + '@noble/curves@1.9.6': + resolution: {integrity: sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1272,6 +1307,12 @@ packages: '@scure/base@1.2.6': resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -1343,6 +1384,10 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@types/bip39@3.0.4': + resolution: {integrity: sha512-kgmgxd14vTUMqcKu/gRi7adMchm7teKnOzdkeP0oQ5QovXpbUJISU0KUtBt84DdxCws/YuNlSCIoZqgXexe6KQ==} + deprecated: This is a stub types definition. bip39 provides its own type definitions, so you do not need this installed. + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -1397,6 +1442,12 @@ packages: '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + '@types/libsodium-wrappers-sumo@0.7.8': + resolution: {integrity: sha512-N2+df4MB/A+W0RAcTw7A5oxKgzD+Vh6Ye7lfjWIi5SdTzVLfHPzxUjhwPqHLO5Ev9fv/+VHl+sUaUuTg4fUPqw==} + + '@types/libsodium-wrappers@0.7.14': + resolution: {integrity: sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ==} + '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -1830,6 +1881,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bip39@3.1.0: + resolution: {integrity: sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -3435,6 +3489,12 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libsodium-sumo@0.7.15: + resolution: {integrity: sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==} + + libsodium-wrappers-sumo@0.7.15: + resolution: {integrity: sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==} + list-item@1.1.1: resolution: {integrity: sha512-S3D0WZ4J6hyM8o5SNKWaMYB1ALSacPZ2nHGEuCjmHZ+dc03gFeNZoNDcqfcnO4vDhTZmNrqrpYZCdXsRh22bzw==} engines: {node: '>=0.10.0'} @@ -4960,8 +5020,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.12.0: - resolution: {integrity: sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==} + undici@7.13.0: + resolution: {integrity: sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==} engines: {node: '>=20.18.1'} unified@10.1.2: @@ -5578,6 +5638,8 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dcspark/cardano-multiplatform-lib-nodejs@6.2.0': {} + '@dependents/detective-less@5.0.1': dependencies: gonzales-pe: 4.3.0 @@ -5587,12 +5649,12 @@ snapshots: '@dprint/typescript@0.91.8': {} - '@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3)': + '@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3)': dependencies: '@effect/platform': 0.90.0(effect@3.17.3) - '@effect/rpc': 0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) - '@effect/sql': 0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) - '@effect/workflow': 0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) + '@effect/rpc': 0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) + '@effect/sql': 0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) + '@effect/workflow': 0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) effect: 3.17.3 '@effect/docgen@0.5.2(tsx@4.20.3)(typescript@5.8.3)': @@ -5610,7 +5672,7 @@ snapshots: '@dprint/typescript': 0.91.8 prettier-linter-helpers: 1.0.0 - '@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3)': + '@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3)': dependencies: '@effect/platform': 0.90.0(effect@3.17.3) effect: 3.17.3 @@ -5631,12 +5693,12 @@ snapshots: repeat-string: 1.6.1 strip-color: 0.1.0 - '@effect/platform-node-shared@0.47.0(@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3)': + '@effect/platform-node-shared@0.47.0(@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3)': dependencies: - '@effect/cluster': 0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) + '@effect/cluster': 0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) '@effect/platform': 0.90.0(effect@3.17.3) - '@effect/rpc': 0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) - '@effect/sql': 0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) + '@effect/rpc': 0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) + '@effect/sql': 0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) '@parcel/watcher': 2.5.1 effect: 3.17.3 multipasta: 0.2.7 @@ -5645,16 +5707,16 @@ snapshots: - bufferutil - utf-8-validate - '@effect/platform-node@0.94.0(@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3)': + '@effect/platform-node@0.94.1(@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3)': dependencies: - '@effect/cluster': 0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) + '@effect/cluster': 0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) '@effect/platform': 0.90.0(effect@3.17.3) - '@effect/platform-node-shared': 0.47.0(@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) - '@effect/rpc': 0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) - '@effect/sql': 0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) + '@effect/platform-node-shared': 0.47.0(@effect/cluster@0.46.2(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3) + '@effect/rpc': 0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) + '@effect/sql': 0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) effect: 3.17.3 mime: 3.0.0 - undici: 7.12.0 + undici: 7.13.0 ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -5668,14 +5730,14 @@ snapshots: msgpackr: 1.11.5 multipasta: 0.2.7 - '@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3)': + '@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3)': dependencies: '@effect/platform': 0.90.0(effect@3.17.3) effect: 3.17.3 - '@effect/sql@0.44.0(@effect/experimental@0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3)': + '@effect/sql@0.44.0(@effect/experimental@0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3)': dependencies: - '@effect/experimental': 0.54.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) + '@effect/experimental': 0.54.3(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) '@effect/platform': 0.90.0(effect@3.17.3) '@opentelemetry/semantic-conventions': 1.36.0 effect: 3.17.3 @@ -5686,10 +5748,10 @@ snapshots: effect: 3.17.3 vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.9) - '@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3)': + '@effect/workflow@0.8.1(@effect/platform@0.90.0(effect@3.17.3))(@effect/rpc@0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3))(effect@3.17.3)': dependencies: '@effect/platform': 0.90.0(effect@3.17.3) - '@effect/rpc': 0.68.0(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) + '@effect/rpc': 0.68.2(@effect/platform@0.90.0(effect@3.17.3))(effect@3.17.3) effect: 3.17.3 '@emnapi/core@1.4.5': @@ -6137,6 +6199,12 @@ snapshots: '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': optional: true + '@noble/curves@1.9.6': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6303,6 +6371,17 @@ snapshots: '@scure/base@1.2.6': {} + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.6 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@standard-schema/spec@1.0.0': {} '@swc/counter@0.1.3': {} @@ -6401,6 +6480,10 @@ snapshots: dependencies: '@types/estree': 1.0.8 + '@types/bip39@3.0.4': + dependencies: + bip39: 3.1.0 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -6462,6 +6545,12 @@ snapshots: '@types/katex@0.16.7': {} + '@types/libsodium-wrappers-sumo@0.7.8': + dependencies: + '@types/libsodium-wrappers': 0.7.14 + + '@types/libsodium-wrappers@0.7.14': {} + '@types/mdast@3.0.15': dependencies: '@types/unist': 2.0.11 @@ -6933,6 +7022,10 @@ snapshots: binary-extensions@2.3.0: optional: true + bip39@3.1.0: + dependencies: + '@noble/hashes': 1.8.0 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -8852,6 +8945,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libsodium-sumo@0.7.15: {} + + libsodium-wrappers-sumo@0.7.15: + dependencies: + libsodium-sumo: 0.7.15 + list-item@1.1.1: dependencies: expand-range: 1.8.2 @@ -10851,7 +10950,7 @@ snapshots: undici-types@6.21.0: {} - undici@7.12.0: {} + undici@7.13.0: {} unified@10.1.2: dependencies: From 998a3f8f03954934f1d480bc97feb08991c665a8 Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Sun, 10 Aug 2025 08:52:49 -0600 Subject: [PATCH 2/6] feat: upgrade modules --- packages/evolution/src/Certificate.ts | 14 +- packages/evolution/src/DRep.ts | 24 +- packages/evolution/src/GovernanceAction.ts | 28 +- packages/evolution/src/Mint.ts | 12 +- packages/evolution/src/Pointer.ts | 2 +- packages/evolution/src/PrivateKey.ts | 4 +- packages/evolution/src/ProposalProcedure.ts | 289 ++++++++++++++++++ packages/evolution/src/ProposalProcedures.ts | 111 ++----- packages/evolution/src/TransactionBody.ts | 209 +++++++------ packages/evolution/src/VotingProcedures.ts | 257 ++++++++-------- packages/evolution/src/index.ts | 1 + packages/evolution/test/Address.test.ts | 97 +++--- .../test/ProposalProcedures.CML.test.ts | 79 +++++ .../evolution/test/RewardAccount.CML.test.ts | 41 +++ .../test/VotingProcedures.CML.test.ts | 120 ++++++++ 15 files changed, 887 insertions(+), 401 deletions(-) create mode 100644 packages/evolution/src/ProposalProcedure.ts create mode 100644 packages/evolution/test/ProposalProcedures.CML.test.ts create mode 100644 packages/evolution/test/RewardAccount.CML.test.ts create mode 100644 packages/evolution/test/VotingProcedures.CML.test.ts diff --git a/packages/evolution/src/Certificate.ts b/packages/evolution/src/Certificate.ts index be36dc77..7b48c7e5 100644 --- a/packages/evolution/src/Certificate.ts +++ b/packages/evolution/src/Certificate.ts @@ -176,24 +176,14 @@ export const CDDLSchema = Schema.Union( Schema.Tuple( Schema.Literal(9n), Credential.CDDLSchema, - Schema.Union( - Schema.Tuple(Schema.Literal(0), Schema.Uint8ArrayFromSelf), - Schema.Tuple(Schema.Literal(1), Schema.Uint8ArrayFromSelf), - Schema.Tuple(Schema.Literal(2)), - Schema.Tuple(Schema.Literal(3)) - ) + DRep.CDDLSchema ), // 10: stake_vote_deleg_cert = (10, stake_credential, pool_keyhash, drep) Schema.Tuple( Schema.Literal(10n), Credential.CDDLSchema, CBOR.ByteArray, - Schema.Union( - Schema.Tuple(Schema.Literal(0), Schema.Uint8ArrayFromSelf), - Schema.Tuple(Schema.Literal(1), Schema.Uint8ArrayFromSelf), - Schema.Tuple(Schema.Literal(2)), - Schema.Tuple(Schema.Literal(3)) - ) + DRep.CDDLSchema, ), // 11: stake_reg_deleg_cert = (11, stake_credential, pool_keyhash, coin) Schema.Tuple(Schema.Literal(11n), Credential.CDDLSchema, CBOR.ByteArray, CBOR.Integer), diff --git a/packages/evolution/src/DRep.ts b/packages/evolution/src/DRep.ts index 60661205..743c49ba 100644 --- a/packages/evolution/src/DRep.ts +++ b/packages/evolution/src/DRep.ts @@ -44,10 +44,10 @@ export const DRep = Schema.Union( export type DRep = typeof DRep.Type export const CDDLSchema = Schema.Union( - Schema.Tuple(Schema.Literal(0), Schema.Uint8ArrayFromSelf), - Schema.Tuple(Schema.Literal(1), Schema.Uint8ArrayFromSelf), - Schema.Tuple(Schema.Literal(2)), - Schema.Tuple(Schema.Literal(3)) + Schema.Tuple(Schema.Literal(0n), Schema.Uint8ArrayFromSelf), + Schema.Tuple(Schema.Literal(1n), Schema.Uint8ArrayFromSelf), + Schema.Tuple(Schema.Literal(2n)), + Schema.Tuple(Schema.Literal(3n)) ) /** @@ -64,41 +64,41 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(DRe switch (toA._tag) { case "KeyHashDRep": { const keyHashBytes = yield* ParseResult.encode(KeyHash.FromBytes)(toA.keyHash) - return [0, keyHashBytes] as const + return [0n, keyHashBytes] as const } case "ScriptHashDRep": { const scriptHashBytes = yield* ParseResult.encode(ScriptHash.FromBytes)(toA.scriptHash) - return [1, scriptHashBytes] as const + return [1n, scriptHashBytes] as const } case "AlwaysAbstainDRep": - return [2] as const + return [2n] as const case "AlwaysNoConfidenceDRep": - return [3] as const + return [3n] as const } }), decode: (fromA) => Eff.gen(function* () { const [tag, ...rest] = fromA switch (tag) { - case 0: { + case 0n: { const keyHash = yield* ParseResult.decode(KeyHash.FromBytes)(rest[0] as Uint8Array) return yield* ParseResult.decode(DRep)({ _tag: "KeyHashDRep", keyHash }) } - case 1: { + case 1n: { const scriptHash = yield* ParseResult.decode(ScriptHash.FromBytes)(rest[0] as Uint8Array) return yield* ParseResult.decode(DRep)({ _tag: "ScriptHashDRep", scriptHash }) } - case 2: + case 2n: return yield* ParseResult.decode(DRep)({ _tag: "AlwaysAbstainDRep" }) - case 3: + case 3n: return yield* ParseResult.decode(DRep)({ _tag: "AlwaysNoConfidenceDRep" }) diff --git a/packages/evolution/src/GovernanceAction.ts b/packages/evolution/src/GovernanceAction.ts index 537b5530..701e05f2 100644 --- a/packages/evolution/src/GovernanceAction.ts +++ b/packages/evolution/src/GovernanceAction.ts @@ -89,7 +89,7 @@ export class ParameterChangeAction extends Schema.TaggedClass Eff.gen(function* () { @@ -157,7 +157,7 @@ export class HardForkInitiationAction extends Schema.TaggedClass Eff.gen(function* () { @@ -300,7 +300,7 @@ export class NoConfidenceAction extends Schema.TaggedClass() * @category schemas */ export const NoConfidenceActionCDDL = Schema.Tuple( - Schema.Literal(3), // action type + Schema.Literal(3n), // action type Schema.NullOr(GovActionIdCDDL) // gov_action_id / nil ) @@ -322,7 +322,7 @@ export const NoConfidenceActionFromCDDL = Schema.transformOrFail( : null // Return as CBOR tuple - return [3, govActionId] as const + return [3n, govActionId] as const }), decode: (cddl) => Eff.gen(function* () { @@ -359,7 +359,7 @@ export class UpdateCommitteeAction extends Schema.TaggedClass CBOR.MapSchema, // { * committee_cold_credential => committee_hot_credential } @@ -387,7 +387,7 @@ export const UpdateCommitteeActionFromCDDL = Schema.transformOrFail( const threshold = yield* ParseResult.encode(CBOR.CBORSchema)(action.threshold) // Return as CBOR tuple - return [4, govActionId, membersToRemove, membersToAdd, threshold] as const + return [4n, govActionId, membersToRemove, membersToAdd, threshold] as const }), decode: (cddl) => Eff.gen(function* () { @@ -425,7 +425,7 @@ export class NewConstitutionAction extends Schema.TaggedClass Eff.gen(function* () { @@ -483,7 +483,7 @@ export class InfoAction extends Schema.TaggedClass()("InfoAction", { * @category schemas */ export const InfoActionCDDL = Schema.Tuple( - Schema.Literal(6) // action type + Schema.Literal(6n) // action type ) /** @@ -497,7 +497,7 @@ export const InfoActionFromCDDL = Schema.transformOrFail(InfoActionCDDL, Schema. encode: (_action) => Eff.gen(function* () { // Return as CBOR tuple - return [6] as const + return [6n] as const }), decode: (_cddl) => Eff.gen(function* () { diff --git a/packages/evolution/src/Mint.ts b/packages/evolution/src/Mint.ts index fd483d3d..00a44c0e 100644 --- a/packages/evolution/src/Mint.ts +++ b/packages/evolution/src/Mint.ts @@ -219,10 +219,10 @@ export const policyCount = (mint: Mint): number => mint.size export const equals = (self: Mint, that: Mint): boolean => Equal.equals(self, that) export const CDDLSchema = Schema.MapFromSelf({ - key: CBOR.ByteArray, // Policy ID as Uint8Array (28 bytes) + key: CBOR.ByteArray, // Policy ID as 28-byte Uint8Array value: Schema.MapFromSelf({ key: CBOR.ByteArray, // Asset name as Uint8Array (variable length) - value: CBOR.Integer // Amount as number (will be converted to NonZeroInt64) + value: CBOR.Integer // Amount as nonZeroInt64 }) }) @@ -240,16 +240,16 @@ export const CDDLSchema = Schema.MapFromSelf({ * @since 2.0.0 * @category schemas */ -export const MintCDDLSchema = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Mint), { +export const FromCDDL = Schema.transformOrFail(Schema.encodedSchema(CDDLSchema), Schema.typeSchema(Mint), { strict: true, encode: (toA) => Eff.gen(function* () { // Convert Mint to raw Map data for CBOR encoding - const outerMap = new Map>() + const outerMap = new Map() as Map> for (const [policyId, assetMap] of toA.entries()) { const policyIdBytes = yield* ParseResult.encode(PolicyId.FromBytes)(policyId) - const innerMap = new Map() + const innerMap = new Map() as Map for (const [assetName, amount] of assetMap.entries()) { const assetNameBytes = yield* ParseResult.encode(AssetName.FromBytes)(assetName) @@ -294,7 +294,7 @@ export const MintCDDLSchema = Schema.transformOrFail(CDDLSchema, Schema.typeSche export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - MintCDDLSchema // CBOR → Mint + FromCDDL // CBOR → Mint ).annotations({ identifier: "Mint.FromCBORBytes", title: "Mint from CBOR Bytes", diff --git a/packages/evolution/src/Pointer.ts b/packages/evolution/src/Pointer.ts index d2f0beaf..4db0fda4 100644 --- a/packages/evolution/src/Pointer.ts +++ b/packages/evolution/src/Pointer.ts @@ -1,4 +1,4 @@ -import { Schema, FastCheck } from "effect" +import { FastCheck,Schema } from "effect" import * as Natural from "./Natural.js" diff --git a/packages/evolution/src/PrivateKey.ts b/packages/evolution/src/PrivateKey.ts index 90843b9e..6cce0e14 100644 --- a/packages/evolution/src/PrivateKey.ts +++ b/packages/evolution/src/PrivateKey.ts @@ -5,7 +5,9 @@ import { wordlist } from "@scure/bip39/wordlists/english" import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" import sodium from "libsodium-wrappers-sumo" -import { Bytes32, Bytes64, VKey } from "./index.js" +import * as Bytes32 from "./Bytes32.js" +import * as Bytes64 from "./Bytes64.js" +import * as VKey from "./VKey.js" /** * Error class for PrivateKey related operations. diff --git a/packages/evolution/src/ProposalProcedure.ts b/packages/evolution/src/ProposalProcedure.ts new file mode 100644 index 00000000..112fe15b --- /dev/null +++ b/packages/evolution/src/ProposalProcedure.ts @@ -0,0 +1,289 @@ +import { Data, Effect as Eff, ParseResult, Schema } from "effect" + +import * as Anchor from "./Anchor.js" +import * as Bytes from "./Bytes.js" +import * as CBOR from "./CBOR.js" +import * as Coin from "./Coin.js" +import * as GovernanceAction from "./GovernanceAction.js" +import * as RewardAccount from "./RewardAccount.js" + +/** + * Error class for ProposalProcedure related operations. + * + * @since 2.0.0 + * @category errors + */ +export class ProposalProcedureError extends Data.TaggedError("ProposalProcedureError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Schema for a single proposal procedure based on Conway CDDL specification. + * + * ``` + * proposal_procedure = [ + * deposit : coin, + * reward_account : reward_account, + * governance_action : governance_action, + * anchor : anchor / null + * ] + * + * governance_action = [action_type, action_data] + * ``` + * + * @since 2.0.0 + * @category model + */ +export class ProposalProcedure extends Schema.Class("ProposalProcedure")({ + deposit: Coin.Coin, + rewardAccount: RewardAccount.RewardAccount, + governanceAction: GovernanceAction.GovernanceAction, + anchor: Schema.NullOr(Anchor.Anchor) +}) {} + +/** + * CDDL schema for ProposalProcedure tuple structure. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = Schema.Tuple( + CBOR.Integer, // deposit: coin + CBOR.ByteArray, // reward_account (raw bytes) + Schema.encodedSchema(GovernanceAction.CDDLSchema), // governance_action using proper CDDL schema + Schema.NullOr(Anchor.CDDLSchema) // anchor / null +) + +/** + * CDDL transformation schema for individual ProposalProcedure. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transformOrFail( + CDDLSchema, + Schema.typeSchema(ProposalProcedure), + { + strict: true, + encode: (procedure) => + Eff.gen(function* () { + const depositBigInt = BigInt(procedure.deposit) + const rewardAccountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(procedure.rewardAccount) + const governanceActionCDDL = yield* ParseResult.encode(GovernanceAction.FromCDDL)(procedure.governanceAction) + const anchorCDDL = procedure.anchor ? yield* ParseResult.encode(Anchor.FromCDDL)(procedure.anchor) : null + return [depositBigInt, rewardAccountBytes, governanceActionCDDL, anchorCDDL] as const + }), + decode: (procedureTuple) => + Eff.gen(function* () { + const [depositBigInt, rewardAccountBytes, governanceActionCDDL, anchorCDDL] = procedureTuple as any + const deposit = Coin.make(depositBigInt) + const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(rewardAccountBytes) + const governanceAction = yield* ParseResult.decode(GovernanceAction.FromCDDL)(governanceActionCDDL) + const anchor = anchorCDDL ? yield* ParseResult.decode(Anchor.FromCDDL)(anchorCDDL) : null + + return new ProposalProcedure({ + deposit, + rewardAccount, + governanceAction, + anchor + }) + }) + } +) + +/** + * CBOR bytes transformation schema for individual ProposalProcedure. + * + * @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 → ProposalProcedure + ).annotations({ + identifier: "ProposalProcedure.FromCBORBytes", + title: "ProposalProcedure from CBOR Bytes", + description: "Transforms CBOR bytes to ProposalProcedure" + }) + +/** + * CBOR hex transformation schema for individual ProposalProcedure. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose( + Bytes.FromHex, // string → Uint8Array + FromCBORBytes(options) // Uint8Array → ProposalProcedure + ).annotations({ + identifier: "ProposalProcedure.FromCBORHex", + title: "ProposalProcedure from CBOR Hex", + description: "Transforms CBOR hex string to ProposalProcedure" + }) + +/** + * Check if two ProposalProcedure instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: ProposalProcedure, b: ProposalProcedure): boolean => + a.deposit === b.deposit && + RewardAccount.equals(a.rewardAccount, b.rewardAccount) && + GovernanceAction.equals(a.governanceAction, b.governanceAction) && + ((a.anchor === null && b.anchor === null) || + (a.anchor !== null && b.anchor !== null && Anchor.equals(a.anchor, b.anchor))) + +/** + * Create a single ProposalProcedure. + * + * @since 2.0.0 + * @category constructors + */ +export const make = (params: { + deposit: Coin.Coin + rewardAccount: RewardAccount.RewardAccount + governanceAction: GovernanceAction.GovernanceAction + anchor?: Anchor.Anchor | null +}): ProposalProcedure => + new ProposalProcedure({ + deposit: params.deposit, + rewardAccount: params.rewardAccount, + governanceAction: params.governanceAction, + anchor: params.anchor ?? null + }) + +// ============================================================================ +// Root Functions +// ============================================================================ + +/** + * Parse individual ProposalProcedure from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): ProposalProcedure => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse individual ProposalProcedure from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): ProposalProcedure => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Encode individual ProposalProcedure to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (procedure: ProposalProcedure, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(procedure, options)) + +/** + * Encode individual ProposalProcedure to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (procedure: ProposalProcedure, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(procedure, options)) + +// ============================================================================ +// Effect Namespace +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse ProposalProcedure from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new ProposalProcedureError({ + message: "Failed to parse ProposalProcedure from bytes", + cause + }) + ) + ) + + /** + * Parse ProposalProcedure from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new ProposalProcedureError({ + message: "Failed to parse ProposalProcedure from hex", + cause + }) + ) + ) + + /** + * Encode ProposalProcedure to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = ( + procedure: ProposalProcedure, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORBytes(options))(procedure).pipe( + Eff.mapError( + (cause) => + new ProposalProcedureError({ + message: "Failed to encode ProposalProcedure to bytes", + cause + }) + ) + ) + + /** + * Encode ProposalProcedure to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = ( + procedure: ProposalProcedure, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORHex(options))(procedure).pipe( + Eff.mapError( + (cause) => + new ProposalProcedureError({ + message: "Failed to encode ProposalProcedure to hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/ProposalProcedures.ts b/packages/evolution/src/ProposalProcedures.ts index 6b999f25..4a723d7e 100644 --- a/packages/evolution/src/ProposalProcedures.ts +++ b/packages/evolution/src/ProposalProcedures.ts @@ -5,6 +5,7 @@ import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" import * as Coin from "./Coin.js" import * as GovernanceAction from "./GovernanceAction.js" +import * as ProposalProcedure from "./ProposalProcedure.js" import * as RewardAccount from "./RewardAccount.js" /** @@ -18,30 +19,6 @@ export class ProposalProceduresError extends Data.TaggedError("ProposalProcedure cause?: unknown }> {} -/** - * Schema for a single proposal procedure based on Conway CDDL specification. - * - * ``` - * proposal_procedure = [ - * deposit : coin, - * reward_account : reward_account, - * governance_action : governance_action, - * anchor : anchor / null - * ] - * - * governance_action = [action_type, action_data] - * ``` - * - * @since 2.0.0 - * @category model - */ -export class ProposalProcedure extends Schema.Class("ProposalProcedure")({ - deposit: Coin.Coin, - rewardAccount: RewardAccount.RewardAccount, - governanceAction: GovernanceAction.GovernanceAction, - anchor: Schema.NullOr(Anchor.Anchor) -}) {} - /** * ProposalProcedures based on Conway CDDL specification. * @@ -53,33 +30,20 @@ export class ProposalProcedure extends Schema.Class("Proposal * @category model */ export class ProposalProcedures extends Schema.Class("ProposalProcedures")({ - procedures: Schema.Array(ProposalProcedure).pipe( + procedures: Schema.Array(ProposalProcedure.ProposalProcedure).pipe( Schema.filter((arr) => arr.length > 0, { message: () => "ProposalProcedures must contain at least one procedure" }) ) }) {} -/** - * CDDL schema for ProposalProcedure tuple structure. - * - * @since 2.0.0 - * @category schemas - */ -export const ProposalProcedureCDDLSchema = Schema.Tuple( - CBOR.Integer, // deposit: coin - CBOR.ByteArray, // reward_account (raw bytes) - GovernanceAction.CDDLSchema, // governance_action using proper CDDL schema - Schema.NullOr(Anchor.CDDLSchema) // anchor / null -) - /** * CDDL schema for ProposalProcedures that produces CBOR-compatible types. * * @since 2.0.0 * @category schemas */ -export const CDDLSchema = Schema.Array(ProposalProcedureCDDLSchema) +export const CDDLSchema = Schema.Array(ProposalProcedure.CDDLSchema) /** * CDDL transformation schema for ProposalProcedures. @@ -90,38 +54,11 @@ export const CDDLSchema = Schema.Array(ProposalProcedureCDDLSchema) export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(ProposalProcedures), { strict: true, encode: (toA) => - Eff.all( - toA.procedures.map((procedure) => - Eff.gen(function* () { - const depositBigInt = BigInt(procedure.deposit) - const rewardAccountBytes = yield* ParseResult.encode(RewardAccount.FromBytes)(procedure.rewardAccount) - const governanceActionCDDL = yield* ParseResult.encode(GovernanceAction.FromCDDL)( - procedure.governanceAction - ) - const anchorCDDL = procedure.anchor ? yield* ParseResult.encode(Anchor.FromCDDL)(procedure.anchor) : null - return [depositBigInt, rewardAccountBytes, governanceActionCDDL, anchorCDDL] as any - }) - ) - ), + Eff.all(toA.procedures.map((procedure) => ParseResult.encode(ProposalProcedure.FromCDDL)(procedure))), decode: (fromA) => Eff.gen(function* () { const procedures = yield* Eff.all( - fromA.map((procedureTuple: any) => - Eff.gen(function* () { - const [depositBigInt, rewardAccountBytes, governanceActionCDDL, anchorCDDL] = procedureTuple - const deposit = Coin.make(depositBigInt) - const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(rewardAccountBytes) - const governanceAction = yield* ParseResult.decode(GovernanceAction.FromCDDL)(governanceActionCDDL) - const anchor = anchorCDDL ? yield* ParseResult.decode(Anchor.FromCDDL)(anchorCDDL) : null - - return new ProposalProcedure({ - deposit, - rewardAccount, - governanceAction, - anchor - }) - }) - ) + fromA.map((procedureTuple ) => ParseResult.decode(ProposalProcedure.FromCDDL)(procedureTuple)) ) return new ProposalProcedures({ procedures }) @@ -170,15 +107,7 @@ export const equals = (a: ProposalProcedures, b: ProposalProcedures): boolean => a.procedures.length === b.procedures.length && a.procedures.every((procedureA, index) => { const procedureB = b.procedures[index] - return ( - procedureA.deposit === procedureB.deposit && - RewardAccount.equals(procedureA.rewardAccount, procedureB.rewardAccount) && - GovernanceAction.equals(procedureA.governanceAction, procedureB.governanceAction) && - ((procedureA.anchor === null && procedureB.anchor === null) || - (procedureA.anchor !== null && - procedureB.anchor !== null && - Anchor.equals(procedureA.anchor, procedureB.anchor))) - ) + return ProposalProcedure.equals(procedureA, procedureB) }) /** @@ -187,7 +116,7 @@ export const equals = (a: ProposalProcedures, b: ProposalProcedures): boolean => * @since 2.0.0 * @category constructors */ -export const make = (procedures: Array): ProposalProcedures => { +export const make = (procedures: Array): ProposalProcedures => { if (procedures.length === 0) { throw new Error("ProposalProcedures must contain at least one procedure") } @@ -205,13 +134,7 @@ export const makeProcedure = (params: { rewardAccount: RewardAccount.RewardAccount governanceAction: GovernanceAction.GovernanceAction anchor?: Anchor.Anchor | null -}): ProposalProcedure => - new ProposalProcedure({ - deposit: params.deposit, - rewardAccount: params.rewardAccount, - governanceAction: params.governanceAction, - anchor: params.anchor ?? null - }) +}): ProposalProcedure.ProposalProcedure => ProposalProcedure.make(params) /** * FastCheck arbitrary for ProposalProcedures. @@ -226,7 +149,7 @@ export const arbitrary = FastCheck.record({ rewardAccount: RewardAccount.arbitrary, governanceAction: GovernanceAction.arbitrary, anchor: FastCheck.option(Anchor.arbitrary, { nil: null }) - }).map((params) => new ProposalProcedure(params)), + }).map((params) => ProposalProcedure.make(params)), { minLength: 1, maxLength: 5 } ) }).map((params) => new ProposalProcedures(params)) @@ -242,7 +165,7 @@ export const arbitrary = FastCheck.record({ * @category parsing */ export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): ProposalProcedures => - Eff.runSync(Effect.fromCBORBytes(bytes, options) as any) + Eff.runSync(Effect.fromCBORBytes(bytes, options)) /** * Parse ProposalProcedures from CBOR hex string. @@ -251,7 +174,7 @@ export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): P * @category parsing */ export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): ProposalProcedures => - Eff.runSync(Effect.fromCBORHex(hex, options) as any) + Eff.runSync(Effect.fromCBORHex(hex, options)) /** * Encode ProposalProcedures to CBOR bytes. @@ -260,7 +183,7 @@ export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): ProposalP * @category encoding */ export const toCBORBytes = (proposalProcedures: ProposalProcedures, options?: CBOR.CodecOptions): Uint8Array => - Eff.runSync(Effect.toCBORBytes(proposalProcedures, options) as any) + Eff.runSync(Effect.toCBORBytes(proposalProcedures, options)) /** * Encode ProposalProcedures to CBOR hex string. @@ -269,7 +192,7 @@ export const toCBORBytes = (proposalProcedures: ProposalProcedures, options?: CB * @category encoding */ export const toCBORHex = (proposalProcedures: ProposalProcedures, options?: CBOR.CodecOptions): string => - Eff.runSync(Effect.toCBORHex(proposalProcedures, options) as any) + Eff.runSync(Effect.toCBORHex(proposalProcedures, options)) // ============================================================================ // Effect Namespace @@ -291,7 +214,7 @@ export namespace Effect { export const fromCBORBytes = ( bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS - ): Eff.Effect => + ): Eff.Effect => Schema.decode(FromCBORBytes(options))(bytes).pipe( Eff.mapError( (cause) => @@ -311,7 +234,7 @@ export namespace Effect { export const fromCBORHex = ( hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS - ): Eff.Effect => + ): Eff.Effect => Schema.decode(FromCBORHex(options))(hex).pipe( Eff.mapError( (cause) => @@ -331,7 +254,7 @@ export namespace Effect { export const toCBORBytes = ( proposalProcedures: ProposalProcedures, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS - ): Eff.Effect => + ): Eff.Effect => Schema.encode(FromCBORBytes(options))(proposalProcedures).pipe( Eff.mapError( (cause) => @@ -351,7 +274,7 @@ export namespace Effect { export const toCBORHex = ( proposalProcedures: ProposalProcedures, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS - ): Eff.Effect => + ): Eff.Effect => Schema.encode(FromCBORHex(options))(proposalProcedures).pipe( Eff.mapError( (cause) => diff --git a/packages/evolution/src/TransactionBody.ts b/packages/evolution/src/TransactionBody.ts index 30787e50..98d03684 100644 --- a/packages/evolution/src/TransactionBody.ts +++ b/packages/evolution/src/TransactionBody.ts @@ -87,31 +87,26 @@ export class TransactionBodyError extends Data.TaggedError("TransactionBodyError * CDDL schema for TransactionBody map structure. * * Maps the TransactionBody fields to their CDDL field numbers according to Conway spec. + * Uses MapFromSelf to produce CBOR with integer keys (not string keys) for CML compatibility. * * @since 2.0.0 * @category schemas */ -export const CDDLSchema = Schema.Struct({ - 0: Schema.Array(Schema.encodedSchema(TransactionInput.CDDLSchema)), // set - 1: Schema.Array(Schema.encodedSchema(TransactionOutput.CDDLSchema)), // [* transaction_output] - 2: CBOR.Integer, // coin - 3: Schema.optional(CBOR.Integer), // slot_no (ttl) - 4: Schema.optional(Schema.Array(Schema.encodedSchema(Certificate.CDDLSchema))), // certificates - 5: Schema.optional(Withdrawals.CDDLSchema), // withdrawals - 7: Schema.optional(CBOR.ByteArray), // auxiliary_data_hash - 8: Schema.optional(CBOR.Integer), // slot_no (validity_interval_start) - 9: Schema.optional(Schema.encodedSchema(Mint.CDDLSchema)), // mint - 11: Schema.optional(CBOR.ByteArray), // script_data_hash - 13: Schema.optional(Schema.Array(TransactionInput.CDDLSchema)), // nonempty_set - 14: Schema.optional(Schema.Array(CBOR.ByteArray)), // required_signers - 15: Schema.optional(CBOR.Integer), // network_id - 16: Schema.optional(Schema.encodedSchema(TransactionOutput.CDDLSchema)), // transaction_output - 17: Schema.optional(CBOR.Integer), // coin - 18: Schema.optional(Schema.Array(Schema.encodedSchema(TransactionInput.CDDLSchema))), // nonempty_set - 19: Schema.optional(Schema.encodedSchema(VotingProcedures.CDDLSchema)), // voting_procedures - 20: Schema.optional(Schema.encodedSchema(ProposalProcedures.CDDLSchema)), // proposal_procedures - 21: Schema.optional(CBOR.Integer), // coin - 22: Schema.optional(CBOR.Integer) // positive_coin +export const CDDLSchema = Schema.MapFromSelf({ + key: CBOR.Integer, + value: Schema.Union( + Schema.Array(TransactionInput.CDDLSchema), // 0: set, 13: collateral_inputs, 18: reference_inputs + Schema.Array(TransactionOutput.CDDLSchema), // 1: [* transaction_output] + CBOR.Integer, // 2: coin (fee), 3: slot_no (ttl), 8: slot_no (validity_interval_start), 15: network_id, 17: coin (total_collateral), 21: coin (current_treasury_value), 22: positive_coin (donation) + Schema.Array(Certificate.CDDLSchema), // 4: certificates + Withdrawals.CDDLSchema, // 5: withdrawals + CBOR.ByteArray, // 7: auxiliary_data_hash, 11: script_data_hash + Schema.encodedSchema(Mint.CDDLSchema), // 9: mint + Schema.Array(CBOR.ByteArray), // 14: required_signers + TransactionOutput.CDDLSchema, // 16: transaction_output (collateral_return) + Schema.encodedSchema(VotingProcedures.CDDLSchema), // 19: voting_procedures + Schema.encodedSchema(ProposalProcedures.CDDLSchema) // 20: proposal_procedures + ) }) export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(TransactionBody), { @@ -142,7 +137,7 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Tra ? yield* ParseResult.encode(AuxiliaryDataHash.BytesSchema)(toA.auxiliaryDataHash) : undefined const validityIntervalStart = toA.validityIntervalStart - const mint = toA.mint ? yield* ParseResult.encode(Mint.MintCDDLSchema)(toA.mint) : undefined + const mint = toA.mint ? (yield* ParseResult.encode(Mint.FromCDDL)(toA.mint)) : undefined const scriptDataHash = toA.scriptDataHash ? yield* ParseResult.encode(ScriptDataHash.FromBytes)(toA.scriptDataHash) : undefined @@ -169,96 +164,116 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Tra const currentTreasuryValue = toA.currentTreasuryValue const donation = toA.donation - return { - 0: inputs, - 1: outputs, - 2: fee, - 3: ttl, - 4: certificates, - 5: withdrawals, - 7: auxiliaryDataHash, - 8: validityIntervalStart, - 9: mint, - 11: scriptDataHash, - 13: collateralInputs, - 14: requiredSigners, - 15: networkId, - 16: collateralReturn, - 17: totalCollateral, - 18: referenceInputs, - 19: votingProcedures, - 20: proposalProcedures, - 21: currentTreasuryValue, - 22: donation - } + // Create Map with integer keys for CBOR + const map = new Map() + + // Required fields + map.set(0n, inputs) + map.set(1n, outputs) + map.set(2n, fee) + + // Optional fields (only set if defined) + if (ttl !== undefined) map.set(3n, ttl) + if (certificates !== undefined) map.set(4n, certificates) + if (withdrawals !== undefined) map.set(5n, withdrawals) + if (auxiliaryDataHash !== undefined) map.set(7n, auxiliaryDataHash) + if (validityIntervalStart !== undefined) map.set(8n, validityIntervalStart) + if (mint !== undefined) map.set(9n, mint) + if (scriptDataHash !== undefined) map.set(11n, scriptDataHash) + if (collateralInputs !== undefined) map.set(13n, collateralInputs) + if (requiredSigners !== undefined) map.set(14n, requiredSigners) + if (networkId !== undefined) map.set(15n, networkId) + if (collateralReturn !== undefined) map.set(16n, collateralReturn) + if (totalCollateral !== undefined) map.set(17n, totalCollateral) + if (referenceInputs !== undefined) map.set(18n, referenceInputs) + if (votingProcedures !== undefined) map.set(19n, votingProcedures) + if (proposalProcedures !== undefined) map.set(20n, proposalProcedures) + if (currentTreasuryValue !== undefined) map.set(21n, currentTreasuryValue) + if (donation !== undefined) map.set(22n, donation) + + return map }), decode: (fromA) => Eff.gen(function* () { // Required fields + const inputsArray = fromA.get(0n) as Array const inputs = (yield* Eff.all( - fromA[0].map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) + inputsArray.map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) )) as NonEmptyArray + const outputsArray = fromA.get(1n) as Array const outputs = yield* Eff.all( - fromA[1].map((output) => ParseResult.decode(TransactionOutput.FromTransactionOutputCDDLSchema)(output)) + outputsArray.map((output) => ParseResult.decode(TransactionOutput.FromTransactionOutputCDDLSchema)(output)) ) - const fee = fromA[2] + const fee = fromA.get(2n) as bigint // Optional fields - const ttl = fromA[3] + const ttl = fromA.get(3n) as bigint | undefined - const certificates = fromA[4] + const certificatesArray = fromA.get(4n) as Array | undefined + const certificates = certificatesArray ? ((yield* Eff.all( - fromA[4].map((cert) => ParseResult.decode(Certificate.FromCDDL)(cert)) + certificatesArray.map((cert) => ParseResult.decode(Certificate.FromCDDL)(cert)) )) as NonEmptyArray) : undefined let withdrawals: Withdrawals.Withdrawals | undefined - if (fromA[5]) { + const withdrawalsMap = fromA.get(5n) as Map | undefined + if (withdrawalsMap) { const decodedWithdrawals = new Map() - for (const [accountBytes, coinAmount] of fromA[5].entries()) { + for (const [accountBytes, coinAmount] of withdrawalsMap.entries()) { const rewardAccount = yield* ParseResult.decode(RewardAccount.FromBytes)(accountBytes) decodedWithdrawals.set(rewardAccount, coinAmount) } withdrawals = new Withdrawals.Withdrawals({ withdrawals: decodedWithdrawals }) } - const auxiliaryDataHash = fromA[7] - ? yield* ParseResult.decode(AuxiliaryDataHash.BytesSchema)(fromA[7]) + const auxiliaryDataHashBytes = fromA.get(7n) as Uint8Array | undefined + const auxiliaryDataHash = auxiliaryDataHashBytes + ? yield* ParseResult.decode(AuxiliaryDataHash.BytesSchema)(auxiliaryDataHashBytes) : undefined - const validityIntervalStart = fromA[8] - const mint = fromA[9] ? yield* ParseResult.decode(Mint.MintCDDLSchema)(fromA[9]) : undefined - const scriptDataHash = fromA[11] ? yield* ParseResult.decode(ScriptDataHash.FromBytes)(fromA[11]) : undefined + const validityIntervalStart = fromA.get(8n) as bigint | undefined + const mintData = fromA.get(9n) as ReadonlyMap> | undefined + const mint = mintData ? yield* ParseResult.decode(Mint.FromCDDL)(mintData) : undefined + const scriptDataHashBytes = fromA.get(11n) as Uint8Array | undefined + const scriptDataHash = scriptDataHashBytes ? yield* ParseResult.decode(ScriptDataHash.FromBytes)(scriptDataHashBytes) : undefined - const collateralInputs = fromA[13] + const collateralInputsArray = fromA.get(13n) as Array | undefined + const collateralInputs = collateralInputsArray ? ((yield* Eff.all( - fromA[13].map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) + collateralInputsArray.map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) )) as NonEmptyArray) : undefined - const requiredSigners = fromA[14] + const requiredSignersArray = fromA.get(14n) as Array | undefined + const requiredSigners = requiredSignersArray ? ((yield* Eff.all( - fromA[14].map((signer) => ParseResult.decode(KeyHash.FromBytes)(signer)) + requiredSignersArray.map((signer) => ParseResult.decode(KeyHash.FromBytes)(signer)) )) as NonEmptyArray) : undefined - const networkId = fromA[15] ? NetworkId.make(Number(fromA[15])) : undefined - const collateralReturn = fromA[16] - ? yield* ParseResult.decode(TransactionOutput.FromTransactionOutputCDDLSchema)(fromA[16]) + const networkIdBigInt = fromA.get(15n) as bigint | undefined + const networkId = networkIdBigInt ? NetworkId.make(Number(networkIdBigInt)) : undefined + const collateralReturnData = fromA.get(16n) as any + const collateralReturn = collateralReturnData + ? yield* ParseResult.decode(TransactionOutput.FromTransactionOutputCDDLSchema)(collateralReturnData) : undefined - const totalCollateral = fromA[17] + const totalCollateral = fromA.get(17n) as bigint | undefined - const referenceInputs = fromA[18] + const referenceInputsArray = fromA.get(18n) as Array | undefined + const referenceInputs = referenceInputsArray ? ((yield* Eff.all( - fromA[18].map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) + referenceInputsArray.map((input) => ParseResult.decode(TransactionInput.FromCDDL)(input)) )) as NonEmptyArray) : undefined - const votingProcedures = fromA[19] ? yield* ParseResult.decode(VotingProcedures.FromCDDL)(fromA[19]) : undefined - const proposalProcedures = fromA[20] - ? yield* ParseResult.decode(ProposalProcedures.FromCDDL)(fromA[20]) + const votingProceduresData = fromA.get(19n) as any + const votingProcedures = votingProceduresData ? yield* ParseResult.decode(VotingProcedures.FromCDDL)(votingProceduresData) : undefined + const proposalProceduresData = fromA.get(20n) as any + const proposalProcedures = proposalProceduresData + ? yield* ParseResult.decode(ProposalProcedures.FromCDDL)(proposalProceduresData) : undefined - const currentTreasuryValue = fromA[21] - const donation = fromA[22] + const currentTreasuryValue = fromA.get(21n) as bigint | undefined + const donation = fromA.get(22n) as bigint | undefined return new TransactionBody({ inputs, @@ -293,10 +308,7 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Tra * @category schemas */ export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => - Schema.compose( - CBOR.FromBytes(options), - FromCDDL - ).annotations({ + Schema.compose(CBOR.FromBytes(options), FromCDDL).annotations({ identifier: "TransactionBody.FromCBORBytes", title: "TransactionBody from CBOR bytes", description: "Decode TransactionBody from CBOR-encoded bytes using Conway CDDL specification" @@ -310,10 +322,7 @@ export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTI * @category schemas */ export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => - Schema.compose( - CBOR.FromHex(options), - FromCDDL - ).annotations({ + Schema.compose(CBOR.FromHex(options), FromCDDL).annotations({ identifier: "TransactionBody.FromCBORHex", title: "TransactionBody from CBOR hex", description: "Decode TransactionBody from CBOR-encoded hex string using Conway CDDL specification" @@ -329,8 +338,10 @@ export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTION * @since 2.0.0 * @category parsing */ -export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): TransactionBody => - Eff.runSync(Effect.fromCBORBytes(bytes, options)) +export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS +) => Eff.runSync(Effect.fromCBORBytes(bytes, options)) /** * Decode a TransactionBody from CBOR hex string. @@ -338,7 +349,7 @@ export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CB * @since 2.0.0 * @category parsing */ -export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): TransactionBody => +export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => Eff.runSync(Effect.fromCBORHex(hex, options)) /** @@ -347,8 +358,10 @@ export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_D * @since 2.0.0 * @category encoding */ -export const toCBORBytes = (transactionBody: TransactionBody, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Uint8Array => - Eff.runSync(Effect.toCBORBytes(transactionBody, options)) +export const toCBORBytes = ( + transactionBody: TransactionBody, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS +) => Eff.runSync(Effect.toCBORBytes(transactionBody, options)) /** * Encode a TransactionBody to CBOR hex string. @@ -356,8 +369,10 @@ export const toCBORBytes = (transactionBody: TransactionBody, options: CBOR.Code * @since 2.0.0 * @category encoding */ -export const toCBORHex = (transactionBody: TransactionBody, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): string => - Eff.runSync(Effect.toCBORHex(transactionBody, options)) +export const toCBORHex = ( + transactionBody: TransactionBody, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS +) => Eff.runSync(Effect.toCBORHex(transactionBody, options)) // ============================================================================ // Effect Namespace @@ -376,7 +391,10 @@ export namespace Effect { * @since 2.0.0 * @category parsing */ - export const fromCBORBytes = (bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => Schema.decode(FromCBORBytes(options))(bytes).pipe( Eff.mapError( (cause) => @@ -393,7 +411,10 @@ export namespace Effect { * @since 2.0.0 * @category parsing */ - export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + export const fromCBORHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => Schema.decode(FromCBORHex(options))(hex).pipe( Eff.mapError( (cause) => @@ -410,7 +431,10 @@ export namespace Effect { * @since 2.0.0 * @category encoding */ - export const toCBORBytes = (transactionBody: TransactionBody, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + export const toCBORBytes = ( + transactionBody: TransactionBody, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => Schema.encode(FromCBORBytes(options))(transactionBody).pipe( Eff.mapError( (cause) => @@ -427,7 +451,10 @@ export namespace Effect { * @since 2.0.0 * @category encoding */ - export const toCBORHex = (transactionBody: TransactionBody, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Eff.Effect => + export const toCBORHex = ( + transactionBody: TransactionBody, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => Schema.encode(FromCBORHex(options))(transactionBody).pipe( Eff.mapError( (cause) => diff --git a/packages/evolution/src/VotingProcedures.ts b/packages/evolution/src/VotingProcedures.ts index 03ea394e..fa02109a 100644 --- a/packages/evolution/src/VotingProcedures.ts +++ b/packages/evolution/src/VotingProcedures.ts @@ -7,6 +7,8 @@ import * as Credential from "./Credential.js" import * as DRep from "./DRep.js" import * as GovernanceAction from "./GovernanceAction.js" import * as PoolKeyHash from "./PoolKeyHash.js" +import * as TransactionHash from "./TransactionHash.js" +import * as TransactionIndex from "./TransactionIndex.js" /** * Error class for VotingProcedures related operations. @@ -21,7 +23,7 @@ export class VotingProceduresError extends Data.TaggedError("VotingProceduresErr /** * Voter types based on Conway CDDL specification. - * + * * ``` * voter = * [ 0, committee_hot_credential ] // Constitutional Committee @@ -32,17 +34,23 @@ export class VotingProceduresError extends Data.TaggedError("VotingProceduresErr * @since 2.0.0 * @category schemas */ -export const ConstitutionalCommitteeVoter = Schema.TaggedStruct("ConstitutionalCommitteeVoter", { - credential: Credential.Credential -}) +// export const ConstitutionalCommitteeVoter = Schema.TaggedStruct("ConstitutionalCommitteeVoter", { +// credential: Credential.Credential +// }) +export class ConstitutionalCommitteeVoter extends Schema.TaggedClass()( + "ConstitutionalCommitteeVoter", + { + credential: Credential.Credential + } +) {} -export const DRepVoter = Schema.TaggedStruct("DRepVoter", { +export class DRepVoter extends Schema.TaggedClass()("DRepVoter", { drep: DRep.DRep -}) +}) {} -export const StakePoolVoter = Schema.TaggedStruct("StakePoolVoter", { +export class StakePoolVoter extends Schema.TaggedClass()("StakePoolVoter", { poolKeyHash: PoolKeyHash.PoolKeyHash -}) +}) {} /** * Voter union schema. @@ -50,13 +58,9 @@ export const StakePoolVoter = Schema.TaggedStruct("StakePoolVoter", { * @since 2.0.0 * @category schemas */ -export const Voter = Schema.Union( - ConstitutionalCommitteeVoter, - DRepVoter, - StakePoolVoter -) +export const Voter = Schema.Union(ConstitutionalCommitteeVoter, DRepVoter, StakePoolVoter) -export type Voter = Schema.Schema.Type +export type Voter = typeof Voter.Type /** * CDDL schema for Voter as tuple structure. @@ -66,9 +70,9 @@ export type Voter = Schema.Schema.Type * @category schemas */ export const VoterCDDL = Schema.Union( - Schema.Tuple(Schema.Literal(0), Credential.CDDLSchema), // committee_hot_credential - Schema.Tuple(Schema.Literal(1), DRep.CDDLSchema), // drep - Schema.Tuple(Schema.Literal(2), CBOR.ByteArray) // pool_keyhash + Schema.Tuple(Schema.Literal(0n), Credential.CDDLSchema), // committee_hot_credential + Schema.Tuple(Schema.Literal(1n), DRep.CDDLSchema), // drep + Schema.Tuple(Schema.Literal(2n), CBOR.ByteArray) // pool_keyhash ) /** @@ -84,15 +88,15 @@ export const VoterFromCDDL = Schema.transformOrFail(VoterCDDL, Schema.typeSchema switch (voter._tag) { case "ConstitutionalCommitteeVoter": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(voter.credential) - return [0, credentialCDDL] as const + return [0n, credentialCDDL] as const } case "DRepVoter": { const drepCDDL = yield* ParseResult.encode(DRep.FromCDDL)(voter.drep) - return [1, drepCDDL] as const + return [1n, drepCDDL] as const } case "StakePoolVoter": { const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(voter.poolKeyHash) - return [2, poolKeyHashBytes] as const + return [2n, poolKeyHashBytes] as const } } }), @@ -100,17 +104,17 @@ export const VoterFromCDDL = Schema.transformOrFail(VoterCDDL, Schema.typeSchema Eff.gen(function* () { const [voterType, voterData] = cddl switch (voterType) { - case 0: { + case 0n: { const credential = yield* ParseResult.decode(Credential.FromCDDL)(voterData) - return { _tag: "ConstitutionalCommitteeVoter", credential } as const + return new ConstitutionalCommitteeVoter({ credential }) } - case 1: { + case 1n: { const drep = yield* ParseResult.decode(DRep.FromCDDL)(voterData) - return { _tag: "DRepVoter", drep } as const + return new DRepVoter({ drep }) } - case 2: { + case 2n: { const poolKeyHash = yield* ParseResult.decode(PoolKeyHash.FromBytes)(voterData) - return { _tag: "StakePoolVoter", poolKeyHash } as const + return new StakePoolVoter({ poolKeyHash }) } default: return yield* ParseResult.fail(new ParseResult.Type(VoterCDDL.ast, cddl)) @@ -120,7 +124,7 @@ export const VoterFromCDDL = Schema.transformOrFail(VoterCDDL, Schema.typeSchema /** * Vote types based on Conway CDDL specification. - * + * * ``` * vote = 0 / 1 / 2 ; No / Yes / Abstain * ``` @@ -150,7 +154,7 @@ export type Vote = typeof Vote.Type */ export const VoteCDDL = Schema.Union( Schema.Literal(0n), // No - Schema.Literal(1n), // Yes + Schema.Literal(1n), // Yes Schema.Literal(2n) // Abstain ) @@ -190,7 +194,7 @@ export const VoteFromCDDL = Schema.transformOrFail(VoteCDDL, Schema.typeSchema(V /** * Voting procedure based on Conway CDDL specification. - * + * * ``` * voting_procedure = [ vote, anchor / null ] * ``` @@ -283,39 +287,39 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Vot encode: (toA) => Eff.gen(function* () { const mapEntries = new Map() - + for (const [voter, govActionMap] of toA.procedures) { const voterCDDL = yield* ParseResult.encode(VoterFromCDDL)(voter) const innerMapEntries = new Map() - + for (const [govActionId, votingProcedure] of govActionMap) { const govActionIdCDDL = yield* ParseResult.encode(GovernanceAction.GovActionIdFromCDDL)(govActionId) const procedureCDDL = yield* ParseResult.encode(VotingProcedureFromCDDL)(votingProcedure) innerMapEntries.set(govActionIdCDDL, procedureCDDL) } - + mapEntries.set(voterCDDL, innerMapEntries) } - + return mapEntries }), decode: (fromA) => Eff.gen(function* () { const proceduresMap = new Map>() - + for (const [voterCDDL, innerMapCDDL] of fromA) { const voter = yield* ParseResult.decode(VoterFromCDDL)(voterCDDL) const govActionMap = new Map() - + for (const [govActionIdCDDL, procedureCDDL] of innerMapCDDL) { const govActionId = yield* ParseResult.decode(GovernanceAction.GovActionIdFromCDDL)(govActionIdCDDL) const procedure = yield* ParseResult.decode(VotingProcedureFromCDDL)(procedureCDDL) govActionMap.set(govActionId, procedure) } - + proceduresMap.set(voter, govActionMap) } - + return new VotingProcedures({ procedures: proceduresMap }) }) }) @@ -381,7 +385,7 @@ export const makeProcedure = (vote: Vote, anchor?: Anchor.Anchor | null): Voting * @category constructors */ export const makeCommitteeVoter = (credential: Credential.Credential): Voter => - ({ _tag: "ConstitutionalCommitteeVoter", credential }) + new ConstitutionalCommitteeVoter({ credential }) /** * Create a DRep voter. @@ -389,8 +393,7 @@ export const makeCommitteeVoter = (credential: Credential.Credential): Voter => * @since 2.0.0 * @category constructors */ -export const makeDRepVoter = (drep: DRep.DRep): Voter => - ({ _tag: "DRepVoter", drep }) +export const makeDRepVoter = (drep: DRep.DRep): DRepVoter => new DRepVoter({ drep }) /** * Create a Stake Pool voter. @@ -398,8 +401,8 @@ export const makeDRepVoter = (drep: DRep.DRep): Voter => * @since 2.0.0 * @category constructors */ -export const makeStakePoolVoter = (poolKeyHash: PoolKeyHash.PoolKeyHash): Voter => - ({ _tag: "StakePoolVoter", poolKeyHash }) +export const makeStakePoolVoter = (poolKeyHash: PoolKeyHash.PoolKeyHash): StakePoolVoter => + new StakePoolVoter({ poolKeyHash }) /** * Create a No vote. @@ -435,8 +438,9 @@ export const abstain = (): Vote => new AbstainVote() * @since 2.0.0 * @category predicates */ -export const isConstitutionalCommitteeVoter = (voter: Voter): voter is Schema.Schema.Type => - voter._tag === "ConstitutionalCommitteeVoter" +export const isConstitutionalCommitteeVoter = ( + voter: Voter +): voter is Schema.Schema.Type => voter._tag === "ConstitutionalCommitteeVoter" /** * Check if a voter is a DRep voter. @@ -452,7 +456,8 @@ export const isDRepVoter = (voter: Voter): voter is Schema.Schema.Type => voter._tag === "StakePoolVoter" +export const isStakePoolVoter = (voter: Voter): voter is Schema.Schema.Type => + voter._tag === "StakePoolVoter" /** * Check if a vote is a No vote. @@ -488,20 +493,22 @@ export const isAbstainVote = (vote: Vote): vote is Schema.Schema.Type(patterns: { - ConstitutionalCommitteeVoter: (credential: Credential.Credential) => R - DRepVoter: (drep: DRep.DRep) => R - StakePoolVoter: (poolKeyHash: PoolKeyHash.PoolKeyHash) => R -}) => (voter: Voter): R => { - switch (voter._tag) { - case "ConstitutionalCommitteeVoter": - return patterns.ConstitutionalCommitteeVoter(voter.credential) - case "DRepVoter": - return patterns.DRepVoter(voter.drep) - case "StakePoolVoter": - return patterns.StakePoolVoter(voter.poolKeyHash) +export const matchVoter = + (patterns: { + ConstitutionalCommitteeVoter: (credential: Credential.Credential) => R + DRepVoter: (drep: DRep.DRep) => R + StakePoolVoter: (poolKeyHash: PoolKeyHash.PoolKeyHash) => R + }) => + (voter: Voter): R => { + switch (voter._tag) { + case "ConstitutionalCommitteeVoter": + return patterns.ConstitutionalCommitteeVoter(voter.credential) + case "DRepVoter": + return patterns.DRepVoter(voter.drep) + case "StakePoolVoter": + return patterns.StakePoolVoter(voter.poolKeyHash) + } } -} /** * Pattern match on a Vote. @@ -509,20 +516,18 @@ export const matchVoter = (patterns: { * @since 2.0.0 * @category pattern matching */ -export const matchVote = (patterns: { - NoVote: () => R - YesVote: () => R - AbstainVote: () => R -}) => (vote: Vote): R => { - switch (vote._tag) { - case "NoVote": - return patterns.NoVote() - case "YesVote": - return patterns.YesVote() - case "AbstainVote": - return patterns.AbstainVote() +export const matchVote = + (patterns: { NoVote: () => R; YesVote: () => R; AbstainVote: () => R }) => + (vote: Vote): R => { + switch (vote._tag) { + case "NoVote": + return patterns.NoVote() + case "YesVote": + return patterns.YesVote() + case "AbstainVote": + return patterns.AbstainVote() + } } -} // ============================================================================ // Equality @@ -536,7 +541,7 @@ export const matchVote = (patterns: { */ export const voterEquals = (a: Voter, b: Voter): boolean => { if (a._tag !== b._tag) return false - + switch (a._tag) { case "ConstitutionalCommitteeVoter": return Credential.equals(a.credential, (b as Schema.Schema.Type).credential) @@ -563,45 +568,49 @@ export const voteEquals = (a: Vote, b: Vote): boolean => a._tag === b._tag */ export const equals = (a: VotingProcedures, b: VotingProcedures): boolean => { if (a.procedures.size !== b.procedures.size) return false - + for (const [voterA, govActionMapA] of a.procedures) { let foundMatchingVoter = false - + for (const [voterB, govActionMapB] of b.procedures) { if (voterEquals(voterA, voterB)) { foundMatchingVoter = true - + if (govActionMapA.size !== govActionMapB.size) return false - + for (const [govActionIdA, procedureA] of govActionMapA) { let foundMatchingAction = false - + for (const [govActionIdB, procedureB] of govActionMapB) { // Simple equality for GovActionId - if (govActionIdA.transactionId === govActionIdB.transactionId && - govActionIdA.govActionIndex === govActionIdB.govActionIndex) { + if ( + govActionIdA.transactionId === govActionIdB.transactionId && + govActionIdA.govActionIndex === govActionIdB.govActionIndex + ) { foundMatchingAction = true - + const votesEqual = voteEquals(procedureA.vote, procedureB.vote) - const anchorsEqual = (procedureA.anchor === null && procedureB.anchor === null) || - (procedureA.anchor !== null && procedureB.anchor !== null && - Anchor.equals(procedureA.anchor, procedureB.anchor)) - + const anchorsEqual = + (procedureA.anchor === null && procedureB.anchor === null) || + (procedureA.anchor !== null && + procedureB.anchor !== null && + Anchor.equals(procedureA.anchor, procedureB.anchor)) + if (!votesEqual || !anchorsEqual) return false break } } - + if (!foundMatchingAction) return false } - + break } } - + if (!foundMatchingVoter) return false } - + return true } @@ -611,34 +620,38 @@ export const equals = (a: VotingProcedures, b: VotingProcedures): boolean => { * @since 2.0.0 * @category arbitrary */ -export const arbitrary = FastCheck.record({ - procedures: FastCheck.array( - FastCheck.tuple( - FastCheck.oneof( - FastCheck.record({ _tag: FastCheck.constant("ConstitutionalCommitteeVoter"), credential: Credential.arbitrary }), - FastCheck.record({ _tag: FastCheck.constant("DRepVoter"), drep: DRep.arbitrary }), - FastCheck.record({ _tag: FastCheck.constant("StakePoolVoter"), poolKeyHash: PoolKeyHash.arbitrary }) - ), - FastCheck.array( +export const arbitrary = FastCheck.array( + FastCheck.tuple( + // Reuse existing voter arbitraries + FastCheck.oneof( + Credential.arbitrary.map(credential => new ConstitutionalCommitteeVoter({ credential })), + DRep.arbitrary.map(drep => new DRepVoter({ drep })), + PoolKeyHash.arbitrary.map(poolKeyHash => new StakePoolVoter({ poolKeyHash })) + ), + FastCheck.array( + FastCheck.tuple( + // Create GovActionId instances using proper branded types FastCheck.tuple( - FastCheck.record({ - _tag: FastCheck.constant("GovActionId"), - transactionId: FastCheck.hexaString({ minLength: 64, maxLength: 64 }), - govActionIndex: FastCheck.integer({ min: 0, max: 65535 }) - }), - FastCheck.record({ - vote: FastCheck.oneof( - FastCheck.constant(new NoVote()), - FastCheck.constant(new YesVote()), - FastCheck.constant(new AbstainVote()) - ), - anchor: FastCheck.option(Anchor.arbitrary, { nil: null }) + FastCheck.hexaString({ minLength: 64, maxLength: 64 }), + FastCheck.integer({ min: 0, max: 65535 }) + ).map(([transactionId, govActionIndex]) => + new GovernanceAction.GovActionId({ + transactionId: TransactionHash.make(transactionId), + govActionIndex: TransactionIndex.make(govActionIndex) }) - ) - ).map(arr => new Map(arr)) - ) - ).map(arr => new Map(arr)) -}) + ), + FastCheck.tuple( + FastCheck.oneof( + FastCheck.constant(new NoVote()), + FastCheck.constant(new YesVote()), + FastCheck.constant(new AbstainVote()) + ), + FastCheck.option(Anchor.arbitrary, { nil: null }) + ).map(([vote, anchor]) => new VotingProcedure({ vote, anchor })) + ) + ).map((arr) => new Map(arr)) + ) +).map((arr) => new VotingProcedures({ procedures: new Map(arr) })) // ============================================================================ // Root Functions @@ -651,7 +664,7 @@ export const arbitrary = FastCheck.record({ * @category parsing */ export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): VotingProcedures => - Eff.runSync(Effect.fromCBORBytes(bytes, options) as any) + Eff.runSync(Effect.fromCBORBytes(bytes, options)) /** * Parse VotingProcedures from CBOR hex string. @@ -660,7 +673,7 @@ export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): V * @category parsing */ export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): VotingProcedures => - Eff.runSync(Effect.fromCBORHex(hex, options) as any) + Eff.runSync(Effect.fromCBORHex(hex, options)) /** * Encode VotingProcedures to CBOR bytes. @@ -669,7 +682,7 @@ export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): VotingPro * @category encoding */ export const toCBORBytes = (votingProcedures: VotingProcedures, options?: CBOR.CodecOptions): Uint8Array => - Eff.runSync(Effect.toCBORBytes(votingProcedures, options) as any) + Eff.runSync(Effect.toCBORBytes(votingProcedures, options)) /** * Encode VotingProcedures to CBOR hex string. @@ -678,7 +691,7 @@ export const toCBORBytes = (votingProcedures: VotingProcedures, options?: CBOR.C * @category encoding */ export const toCBORHex = (votingProcedures: VotingProcedures, options?: CBOR.CodecOptions): string => - Eff.runSync(Effect.toCBORHex(votingProcedures, options) as any) + Eff.runSync(Effect.toCBORHex(votingProcedures, options)) // ============================================================================ // Effect Namespace @@ -700,7 +713,7 @@ export namespace Effect { export const fromCBORBytes = ( bytes: Uint8Array, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS - ): Eff.Effect => + ): Eff.Effect => Schema.decode(FromCBORBytes(options))(bytes).pipe( Eff.mapError( (cause) => @@ -720,7 +733,7 @@ export namespace Effect { export const fromCBORHex = ( hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS - ): Eff.Effect => + ): Eff.Effect => Schema.decode(FromCBORHex(options))(hex).pipe( Eff.mapError( (cause) => @@ -740,7 +753,7 @@ export namespace Effect { export const toCBORBytes = ( votingProcedures: VotingProcedures, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS - ): Eff.Effect => + ): Eff.Effect => Schema.encode(FromCBORBytes(options))(votingProcedures).pipe( Eff.mapError( (cause) => @@ -760,7 +773,7 @@ export namespace Effect { export const toCBORHex = ( votingProcedures: VotingProcedures, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS - ): Eff.Effect => + ): Eff.Effect => Schema.encode(FromCBORHex(options))(votingProcedures).pipe( Eff.mapError( (cause) => diff --git a/packages/evolution/src/index.ts b/packages/evolution/src/index.ts index 401ce04b..7e316d46 100644 --- a/packages/evolution/src/index.ts +++ b/packages/evolution/src/index.ts @@ -75,6 +75,7 @@ export * as PoolParams from "./PoolParams.js" export * as Port from "./Port.js" export * as PositiveCoin from "./PositiveCoin.js" export * as PrivateKey from "./PrivateKey.js" +export * as ProposalProcedure from "./ProposalProcedure.js" export * as ProposalProcedures from "./ProposalProcedures.js" export * as ProtocolVersion from "./ProtocolVersion.js" export * as Relay from "./Relay.js" diff --git a/packages/evolution/test/Address.test.ts b/packages/evolution/test/Address.test.ts index 421e313d..ebae78ae 100644 --- a/packages/evolution/test/Address.test.ts +++ b/packages/evolution/test/Address.test.ts @@ -69,7 +69,8 @@ describe("Address", () => { for (const address of ALL_ADDRESSES) { try { // Direct use of the string without branding - Address.Codec.Decode.bech32(address) + // Address.fromBech32(address) + Address.fromBech32(address) // No error means success } catch (error) { expect.fail(`Failed to decode valid address: ${error}`) @@ -80,11 +81,11 @@ describe("Address", () => { it("should reject invalid addresses", () => { // For invalid addresses, we expect the Decode.bech32 call to throw expect(() => { - Address.Codec.Decode.bech32(INVALID_ADDRESS) + Address.fromBech32(INVALID_ADDRESS) }).toThrow() expect(() => { - Address.Codec.Decode.bech32("") + Address.fromBech32("") }).toThrow() }) @@ -92,7 +93,7 @@ describe("Address", () => { for (const hexAddr of VALID_HEX_ADDRESSES) { try { // Should not throw for valid hex addresses - Address.Codec.Decode.hex(hexAddr) + Address.fromHex(hexAddr) } catch (error) { expect.fail(`Failed to decode valid hex address: ${error}`) } @@ -101,15 +102,15 @@ describe("Address", () => { it("should reject invalid hex addresses", () => { expect(() => { - Address.Codec.Decode.hex("not-a-hex-address") + Address.fromHex("not-a-hex-address") }).toThrow() expect(() => { - Address.Codec.Decode.hex("123xyz") + Address.fromHex("123xyz") }).toThrow() expect(() => { - Address.Codec.Decode.hex("") + Address.fromHex("") }).toThrow() }) }) @@ -118,8 +119,8 @@ describe("Address", () => { it("should encode and decode addresses between bech32 and bytes", () => { for (const address of ALL_ADDRESSES) { try { - const addr = Address.Codec.Decode.bech32(address) - const backToBech32 = Address.Codec.Encode.bech32(addr) + const addr = Address.fromBech32(address) + const backToBech32 = Address.toBech32(addr) // Convert to lowercase for comparison since bech32 is case-insensitive expect(backToBech32.toLowerCase()).toBe(address.toLowerCase()) } catch (error) { @@ -131,8 +132,8 @@ describe("Address", () => { it("should encode and decode addresses between hex and bytes", () => { for (const hexAddr of VALID_HEX_ADDRESSES) { try { - const addr = Address.Codec.Decode.hex(hexAddr) - const backToHex = Address.Codec.Encode.hex(addr) + const addr = Address.fromHex(hexAddr) + const backToHex = Address.toHex(addr) expect(backToHex.toLowerCase()).toBe(hexAddr.toLowerCase()) } catch (error) { expect.fail(`Failed during hex encode/decode cycle: ${error}`) @@ -145,12 +146,12 @@ describe("Address", () => { // Test a couple of addresses try { // bech32 -> bytes -> hex - const addr = Address.Codec.Decode.bech32(address) - const hex = Address.Codec.Encode.hex(addr) + const addr = Address.fromBech32(address) + const hex = Address.toHex(addr) // hex -> bytes -> bech32 - const addrFromHex = Address.Codec.Decode.hex(hex) - const backToBech32 = Address.Codec.Encode.bech32(addrFromHex) + const addrFromHex = Address.fromHex(hex) + const backToBech32 = Address.toBech32(addrFromHex) // Should match the original expect(backToBech32.toLowerCase()).toBe(address.toLowerCase()) @@ -165,8 +166,8 @@ describe("Address", () => { it("should consider the same address equal", () => { for (const address of ALL_ADDRESSES) { try { - const addr1 = Address.Codec.Decode.bech32(address) - const addr2 = Address.Codec.Decode.bech32(address) + const addr1 = Address.fromBech32(address) + const addr2 = Address.fromBech32(address) expect(Address.equals(addr1, addr2)).toBe(true) } catch (error) { expect.fail(`Failed to decode address: ${error}`) @@ -177,8 +178,8 @@ describe("Address", () => { it("should consider different addresses not equal", () => { if (ALL_ADDRESSES.length >= 2) { try { - const addr1 = Address.Codec.Decode.bech32(ALL_ADDRESSES[0]) - const addr2 = Address.Codec.Decode.bech32(ALL_ADDRESSES[1]) + const addr1 = Address.fromBech32(ALL_ADDRESSES[0]) + const addr2 = Address.fromBech32(ALL_ADDRESSES[1]) expect(Address.equals(addr1, addr2)).toBe(false) } catch (error) { expect.fail(`Failed to decode addresses: ${error}`) @@ -192,8 +193,8 @@ describe("Address", () => { const upperCaseAddr = MAINNET_ADDRESSES[0].toUpperCase() try { - const addr1 = Address.Codec.Decode.bech32(lowerCaseAddr) - const addr2 = Address.Codec.Decode.bech32(upperCaseAddr) + const addr1 = Address.fromBech32(lowerCaseAddr) + const addr2 = Address.fromBech32(upperCaseAddr) expect(Address.equals(addr1, addr2)).toBe(true) } catch (error) { expect.fail(`Failed to decode case-modified addresses: ${error}`) @@ -205,19 +206,19 @@ describe("Address", () => { it("should identify address types correctly", () => { try { // Base address (mainnet) - const baseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) + const baseAddr = Address.fromBech32(MAINNET_ADDRESSES[0]) expect(baseAddr._tag).toBe("BaseAddress") // Enterprise address (mainnet) - const enterpriseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[6]) + const enterpriseAddr = Address.fromBech32(MAINNET_ADDRESSES[6]) expect(enterpriseAddr._tag).toBe("EnterpriseAddress") // Reward address (mainnet) - const rewardAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[8]) + const rewardAddr = Address.fromBech32(MAINNET_ADDRESSES[8]) expect(rewardAddr._tag).toBe("RewardAccount") // Pointer address (testnet) - const pointerAddr = Address.Codec.Decode.bech32(TESTNET_ADDRESSES[4]) + const pointerAddr = Address.fromBech32(TESTNET_ADDRESSES[4]) expect(pointerAddr._tag).toBe("PointerAddress") } catch (error) { expect.fail(`Failed to decode address: ${error}`) @@ -227,7 +228,7 @@ describe("Address", () => { it("should correctly identify network IDs", () => { try { // Mainnet address - const mainnetAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) + const mainnetAddr = Address.fromBech32(MAINNET_ADDRESSES[0]) if (mainnetAddr._tag === "BaseAddress") { expect(mainnetAddr.networkId).toBe(1) // Mainnet ID } else { @@ -235,7 +236,7 @@ describe("Address", () => { } // Testnet address - const testnetAddr = Address.Codec.Decode.bech32(TESTNET_ADDRESSES[0]) + const testnetAddr = Address.fromBech32(TESTNET_ADDRESSES[0]) if (testnetAddr._tag === "BaseAddress") { expect(testnetAddr.networkId).toBe(0) // Testnet ID } else { @@ -248,7 +249,7 @@ describe("Address", () => { it("should correctly extract payment credential from base address", () => { try { - const baseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) + const baseAddr = Address.fromBech32(MAINNET_ADDRESSES[0]) if (baseAddr._tag === "BaseAddress") { expect(baseAddr.paymentCredential).toBeDefined() } else { @@ -261,7 +262,7 @@ describe("Address", () => { it("should correctly extract stake credential from base address", () => { try { - const baseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) + const baseAddr = Address.fromBech32(MAINNET_ADDRESSES[0]) if (baseAddr._tag === "BaseAddress") { expect(baseAddr.stakeCredential).toBeDefined() } else { @@ -276,19 +277,19 @@ describe("Address", () => { describe("Error handling", () => { it("should throw errors for invalid addresses", () => { expect(() => { - Address.Codec.Decode.bech32(INVALID_ADDRESS) + Address.fromBech32(INVALID_ADDRESS) }).toThrow() }) it("should throw errors for invalid hex addresses", () => { expect(() => { - Address.Codec.Decode.hex("invalid-hex") + Address.fromHex("invalid-hex") }).toThrow() }) it("should throw errors for empty addresses", () => { expect(() => { - Address.Codec.Decode.bech32("") + Address.fromBech32("") }).toThrow() }) }) @@ -296,19 +297,19 @@ describe("Address", () => { describe("FastCheck generator", () => { it("should generate valid addresses", () => { // Get a sample address from the generator - const generatedAddr = FastCheck.sample(Address.generator, 1)[0] + const generatedAddr = FastCheck.sample(Address.arbitrary, 1)[0] // Check that we can encode it without errors - const bytes = Address.Codec.Encode.bytes(generatedAddr) + const bytes = Address.toBytes(generatedAddr) expect(bytes).toBeDefined() // Verify the address can be encoded to bech32 - const bech32 = Address.Codec.Encode.bech32(generatedAddr) + const bech32 = Address.toBech32(generatedAddr) expect(bech32).toBeDefined() expect(bech32.length).toBeGreaterThan(0) // Verify the address can be encoded to hex - const hex = Address.Codec.Encode.hex(generatedAddr) + const hex = Address.toHex(generatedAddr) expect(hex).toBeDefined() expect(hex.length).toBeGreaterThan(0) }) @@ -317,29 +318,29 @@ describe("Address", () => { describe("Address additional features", () => { it("should handle direct encoding between formats", () => { // First decode a bech32 address to get an Address object - const addr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) + const addr = Address.fromBech32(MAINNET_ADDRESSES[0]) // Encode to bytes - const bytes = Address.Codec.Encode.bytes(addr) + const bytes = Address.toBytes(addr) expect(bytes).toBeDefined() // Encode to hex - const hex = Address.Codec.Encode.hex(addr) + const hex = Address.toHex(addr) expect(hex).toBeDefined() // Encode back to bech32 - const bech32 = Address.Codec.Encode.bech32(addr) + const bech32 = Address.toBech32(addr) expect(bech32).toBeDefined() // The full conversion cycle should preserve the address - const addrFromHex = Address.Codec.Decode.hex(hex) - const bech32FromHex = Address.Codec.Encode.bech32(addrFromHex) + const addrFromHex = Address.fromHex(hex) + const bech32FromHex = Address.toBech32(addrFromHex) expect(bech32FromHex.toLowerCase()).toBe(MAINNET_ADDRESSES[0].toLowerCase()) }) it("should correctly identify address properties", () => { // Process a Base Address (type 0) - const baseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[0]) + const baseAddr = Address.fromBech32(MAINNET_ADDRESSES[0]) if (baseAddr._tag === "BaseAddress") { expect(baseAddr.networkId).toBe(1) // Mainnet expect(baseAddr.paymentCredential).toBeDefined() @@ -349,7 +350,7 @@ describe("Address", () => { } // Process an Enterprise Address (type 6) - const enterpriseAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[6]) + const enterpriseAddr = Address.fromBech32(MAINNET_ADDRESSES[6]) if (enterpriseAddr._tag === "EnterpriseAddress") { expect(enterpriseAddr.networkId).toBe(1) // Mainnet expect(enterpriseAddr.paymentCredential).toBeDefined() @@ -358,7 +359,7 @@ describe("Address", () => { } // Process a Pointer Address (type 4) - const pointerAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[4]) + const pointerAddr = Address.fromBech32(MAINNET_ADDRESSES[4]) if (pointerAddr._tag === "PointerAddress") { expect(pointerAddr.networkId).toBe(1) // Mainnet expect(pointerAddr.paymentCredential).toBeDefined() @@ -368,7 +369,7 @@ describe("Address", () => { } // Process a Reward Account (type 14) - const rewardAddr = Address.Codec.Decode.bech32(MAINNET_ADDRESSES[8]) + const rewardAddr = Address.fromBech32(MAINNET_ADDRESSES[8]) if (rewardAddr._tag === "RewardAccount") { expect(rewardAddr.networkId).toBe(1) // Mainnet expect(rewardAddr.stakeCredential).toBeDefined() @@ -380,7 +381,7 @@ describe("Address", () => { it("should validate address network consistency", () => { // Test mainnet addresses for (const address of MAINNET_ADDRESSES) { - const addr = Address.Codec.Decode.bech32(address) + const addr = Address.fromBech32(address) if ("networkId" in addr) { expect(addr.networkId).toBe(1) } @@ -388,7 +389,7 @@ describe("Address", () => { // Test testnet addresses for (const address of TESTNET_ADDRESSES) { - const addr = Address.Codec.Decode.bech32(address) + const addr = Address.fromBech32(address) if ("networkId" in addr) { expect(addr.networkId).toBe(0) } diff --git a/packages/evolution/test/ProposalProcedures.CML.test.ts b/packages/evolution/test/ProposalProcedures.CML.test.ts new file mode 100644 index 00000000..de02a11c --- /dev/null +++ b/packages/evolution/test/ProposalProcedures.CML.test.ts @@ -0,0 +1,79 @@ +import * as CML from "@dcspark/cardano-multiplatform-lib-nodejs" +import { FastCheck } from "effect" +import { describe, expect, it } from "vitest" + +import * as Anchor from "../src/Anchor.js" +import * as GovernanceAction from "../src/GovernanceAction.js" +import * as ProposalProcedure from "../src/ProposalProcedure.js" +import * as ProposalProcedures from "../src/ProposalProcedures.js" +import * as RewardAccount from "../src/RewardAccount.js" + +/** + * CML compatibility test for ProposalProcedures CBOR serialization. + */ +describe("ProposalProcedures CML Compatibility", () => { + // Test helper to generate deterministic test data using arbitraries + const generateTestRewardAccount = (seed: number = 42): RewardAccount.RewardAccount => + FastCheck.sample(RewardAccount.arbitrary, { seed, numRuns: 1 })[0] + + const generateTestAnchor = (seed: number = 42): Anchor.Anchor => + FastCheck.sample(Anchor.arbitrary, { seed, numRuns: 1 })[0] + + it("validates CBOR hex compatibility: Evolution SDK vs CML serialization", () => { + const deposit = 500000000n + const rewardAccount = generateTestRewardAccount(1) + const anchor = generateTestAnchor(1) + + // Create Evolution SDK ProposalProcedure + const evolutionInfoAction = GovernanceAction.makeInfo() + const evolutionProcedure = ProposalProcedures.makeProcedure({ + deposit, + rewardAccount, + governanceAction: evolutionInfoAction, + anchor + }) + + // Create equivalent CML ProposalProcedure + const rewardAccountBytes = RewardAccount.toBytes(rewardAccount) + const credentialHashBytes = rewardAccountBytes.slice(1, 29) + + // Create the correct CML credential based on Evolution SDK credential type + const credential = rewardAccount.stakeCredential._tag === "KeyHash" + ? CML.Credential.new_pub_key(CML.Ed25519KeyHash.from_raw_bytes(credentialHashBytes)) + : CML.Credential.new_script(CML.ScriptHash.from_raw_bytes(credentialHashBytes)) + + const cmlRewardAddress = CML.RewardAddress.new(rewardAccount.networkId, credential) + const cmlInfoAction = CML.GovAction.new_info_action() + + // Get Evolution SDK anchor CBOR and create CML anchor + const anchorHex = Anchor.toCBORHex(anchor) + const cmlAnchor = CML.Anchor.from_cbor_hex(anchorHex) + + // Create CML ProposalProcedure + const cmlProcedure = CML.ProposalProcedure.new( + deposit, + cmlRewardAddress, + cmlInfoAction, + cmlAnchor + ) + + // Get CBOR hex from both implementations - now comparing individual procedures + const cmlProcedureCborHex = cmlProcedure.to_cbor_hex() + + // Evolution SDK: use individual ProposalProcedure CBOR method + const evolutionProcedureCborHex = ProposalProcedure.toCBORHex(evolutionProcedure) + + // Log both for comparison + // eslint-disable-next-line no-console + console.log("CML individual procedure CBOR (TRUTH):", cmlProcedureCborHex) + // eslint-disable-next-line no-console + console.log("Evolution individual procedure CBOR: ", evolutionProcedureCborHex) + + // CML CBOR is the truth - Evolution SDK must match exactly + expect(evolutionProcedureCborHex).toBe(cmlProcedureCborHex) + + // Test that Evolution SDK can parse CML's CBOR (the truth) + const evolutionRoundTrip = ProposalProcedure.fromCBORHex(cmlProcedureCborHex) + expect(evolutionRoundTrip).toEqual(evolutionProcedure) + }) +}) \ No newline at end of file diff --git a/packages/evolution/test/RewardAccount.CML.test.ts b/packages/evolution/test/RewardAccount.CML.test.ts new file mode 100644 index 00000000..68db2d0f --- /dev/null +++ b/packages/evolution/test/RewardAccount.CML.test.ts @@ -0,0 +1,41 @@ +import * as CML from "@dcspark/cardano-multiplatform-lib-nodejs" +import { describe, expect,it } from "vitest" + +import * as KeyHash from "../src/KeyHash.js" +import * as NetworkId from "../src/NetworkId.js" +import * as RewardAccount from "../src/RewardAccount.js" + +describe("RewardAccount CML Compatibility", () => { + it("validates hex compatibility with CML", () => { + // Create test data + const keyHashBytes = new Uint8Array(28) + for (let i = 0; i < 28; i++) { + keyHashBytes[i] = (i + 5) % 256 // Deterministic test data + } + const keyHashHex = Buffer.from(keyHashBytes).toString('hex') + + // Test both networks + const networks = [0, 1] // testnet, mainnet + + networks.forEach((networkValue) => { + // Create Evolution SDK RewardAccount + const keyHash = KeyHash.fromBytes(keyHashBytes) + const networkId = NetworkId.make(networkValue) + const evolutionRewardAccount = RewardAccount.make({ + networkId, + stakeCredential: { _tag: "KeyHash", hash: keyHash } + }) + + // Create CML RewardAccount - try different API + const cmlKeyHash = CML.Ed25519KeyHash.from_hex(keyHashHex) + const cmlCredential = CML.Credential.new_pub_key(cmlKeyHash) // Try this method + const cmlRewardAccount = CML.RewardAddress.new(networkValue, cmlCredential) + + // Get hex from both (RewardAccount uses raw address bytes, not CBOR) + const evolutionHex = RewardAccount.toHex(evolutionRewardAccount) + const cmlHex = cmlRewardAccount.to_address().to_hex() + // Compare the hex strings + expect(evolutionHex).toBe(cmlHex) + }) + }) +}) diff --git a/packages/evolution/test/VotingProcedures.CML.test.ts b/packages/evolution/test/VotingProcedures.CML.test.ts new file mode 100644 index 00000000..05f2eb41 --- /dev/null +++ b/packages/evolution/test/VotingProcedures.CML.test.ts @@ -0,0 +1,120 @@ +import * as CML from "@dcspark/cardano-multiplatform-lib-nodejs" +import { FastCheck } from "effect" +import { describe, expect, it } from "vitest" + +import * as Anchor from "../src/Anchor.js" +import * as DRep from "../src/DRep.js" +import * as GovernanceAction from "../src/GovernanceAction.js" +import * as TransactionHash from "../src/TransactionHash.js" +import * as TransactionIndex from "../src/TransactionIndex.js" +import * as VotingProcedures from "../src/VotingProcedures.js" + +/** + * CML compatibility test for VotingProcedures CBOR serialization. + */ +describe("VotingProcedures CML Compatibility", () => { + // Test helper to generate deterministic test data using arbitraries + const generateTestDRep = (seed: number = 42): DRep.DRep => + FastCheck.sample(DRep.arbitrary, { seed, numRuns: 1 })[0] + + const generateTestAnchor = (seed: number = 42): Anchor.Anchor => + FastCheck.sample(Anchor.arbitrary, { seed, numRuns: 1 })[0] + + it("validates CBOR hex compatibility: Evolution SDK vs CML serialization", () => { + // Create test data using arbitraries + const drep = generateTestDRep(1) + const anchor = generateTestAnchor(1) + + // Create Evolution SDK VotingProcedures + const drepVoter = VotingProcedures.makeDRepVoter(drep) + const govActionId = new GovernanceAction.GovActionId({ + transactionId: TransactionHash.make("a".repeat(64)), + govActionIndex: TransactionIndex.make(0) + }) + const votingProcedure = VotingProcedures.makeProcedure(VotingProcedures.yes(), anchor) + + const evolutionVotingProcedures = VotingProcedures.make(new Map([ + [drepVoter, new Map([ + [govActionId, votingProcedure] + ])] + ])) + + // Create equivalent CML VotingProcedure + const cmlVote = CML.Vote.Yes + const cmlProcedure = CML.VotingProcedure.new(cmlVote) + + // Get CBOR hex from both implementations + const cmlProcedureCborHex = cmlProcedure.to_cbor_hex() + const evolutionCborHex = VotingProcedures.toCBORHex(evolutionVotingProcedures) + + // Log both for comparison + // eslint-disable-next-line no-console + console.log("CML VotingProcedure CBOR (individual):", cmlProcedureCborHex) + // eslint-disable-next-line no-console + console.log("Evolution VotingProcedures CBOR: ", evolutionCborHex) + + // For Conway governance, CML may not support full VotingProcedures collections yet + // Focus on verifying Evolution SDK produces valid CBOR + expect(evolutionCborHex).toMatch(/^[0-9a-fA-F]+$/) + expect(evolutionCborHex.length).toBeGreaterThan(0) + + // Test that Evolution SDK can parse its own CBOR + const evolutionRoundTrip = VotingProcedures.fromCBORHex(evolutionCborHex) + expect(VotingProcedures.equals(evolutionRoundTrip, evolutionVotingProcedures)).toBe(true) + }) + + it("validates CBOR hex compatibility with anchor: Evolution SDK vs CML serialization", () => { + // Create test data using arbitraries + const drep = generateTestDRep(2) + const anchor = generateTestAnchor(2) + + // Create Evolution SDK VotingProcedures with anchor + const drepVoter = VotingProcedures.makeDRepVoter(drep) + const govActionId = new GovernanceAction.GovActionId({ + transactionId: TransactionHash.make("b".repeat(64)), + govActionIndex: TransactionIndex.make(1) + }) + const votingProcedure = VotingProcedures.makeProcedure(VotingProcedures.no(), anchor) + + const evolutionVotingProcedures = VotingProcedures.make(new Map([ + [drepVoter, new Map([ + [govActionId, votingProcedure] + ])] + ])) + + // Create CML VotingProcedure (without anchor - CML limitation) + const cmlVote = CML.Vote.No + const cmlProcedure = CML.VotingProcedure.new(cmlVote) + + // Test anchor compatibility separately + const anchorHex = Anchor.toCBORHex(anchor) + const cmlAnchor = CML.Anchor.from_cbor_hex(anchorHex) + const cmlAnchorCbor = cmlAnchor.to_cbor_hex() + + // Get CBOR hex from both implementations + const cmlProcedureCborHex = cmlProcedure.to_cbor_hex() + const evolutionCborHex = VotingProcedures.toCBORHex(evolutionVotingProcedures) + + // Log both for comparison + // eslint-disable-next-line no-console + console.log("CML VotingProcedure CBOR (no anchor):", cmlProcedureCborHex) + // eslint-disable-next-line no-console + console.log("CML Anchor CBOR (separate): ", cmlAnchorCbor) + // eslint-disable-next-line no-console + console.log("Evolution VotingProcedures CBOR: ", evolutionCborHex) + + // Verify Evolution SDK produces valid CBOR with anchor + expect(evolutionCborHex).toMatch(/^[0-9a-fA-F]+$/) + expect(evolutionCborHex.length).toBeGreaterThan(0) + expect(evolutionCborHex.length).toBeGreaterThan(cmlProcedureCborHex.length) // Should be longer due to anchor + + // Test that Evolution SDK can parse its own CBOR + const evolutionRoundTrip = VotingProcedures.fromCBORHex(evolutionCborHex) + expect(VotingProcedures.equals(evolutionRoundTrip, evolutionVotingProcedures)).toBe(true) + + // Verify the anchor is preserved + const roundTripEntries = Array.from(evolutionRoundTrip.procedures.values())[0] + const roundTripProcedure = Array.from(roundTripEntries.values())[0] + expect(roundTripProcedure.anchor).not.toBeNull() + }) +}) From 555cdb444a0a52b945096b52eff81745b818c0b4 Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Tue, 12 Aug 2025 06:55:26 -0600 Subject: [PATCH 3/6] feat: add new modules --- packages/evolution/src/AuxiliaryData.ts | 413 ++++++++++++ packages/evolution/src/Certificate.ts | 309 +++++---- packages/evolution/src/Data.ts | 81 ++- packages/evolution/src/Metadata.ts | 397 ++++++++++++ packages/evolution/src/NativeScripts.ts | 88 +-- packages/evolution/src/Numeric.ts | 8 +- packages/evolution/src/PlutusV1.ts | 75 +++ packages/evolution/src/PlutusV2.ts | 74 +++ packages/evolution/src/PlutusV3.ts | 74 +++ packages/evolution/src/Redeemer.ts | 415 ++++++++++++ packages/evolution/src/Script.ts | 175 +++++ packages/evolution/src/Text.ts | 3 + packages/evolution/src/Transaction.ts | 20 +- packages/evolution/src/TransactionBody.ts | 2 +- .../evolution/src/TransactionMetadatum.ts | 449 +++++++++++++ .../evolution/src/TransactionWitnessSet.ts | 600 ++++++++++++++++++ packages/evolution/src/index.ts | 4 + .../evolution/test/AuxiliaryData.CML.test.ts | 100 +++ 18 files changed, 3094 insertions(+), 193 deletions(-) create mode 100644 packages/evolution/src/AuxiliaryData.ts create mode 100644 packages/evolution/src/Metadata.ts create mode 100644 packages/evolution/src/PlutusV1.ts create mode 100644 packages/evolution/src/PlutusV2.ts create mode 100644 packages/evolution/src/PlutusV3.ts create mode 100644 packages/evolution/src/Redeemer.ts create mode 100644 packages/evolution/src/Script.ts create mode 100644 packages/evolution/src/TransactionMetadatum.ts create mode 100644 packages/evolution/src/TransactionWitnessSet.ts create mode 100644 packages/evolution/test/AuxiliaryData.CML.test.ts diff --git a/packages/evolution/src/AuxiliaryData.ts b/packages/evolution/src/AuxiliaryData.ts new file mode 100644 index 00000000..f48a38fb --- /dev/null +++ b/packages/evolution/src/AuxiliaryData.ts @@ -0,0 +1,413 @@ +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" + +import * as Bytes from "./Bytes.js" +import * as CBOR from "./CBOR.js" +import * as Metadata from "./Metadata.js" +import * as NativeScripts from "./NativeScripts.js" +import * as PlutusV1 from "./PlutusV1.js" +import * as PlutusV2 from "./PlutusV2.js" +import * as PlutusV3 from "./PlutusV3.js" + +/** + * Error class for AuxiliaryData related operations. + * + * @since 2.0.0 + * @category errors + */ +export class AuxiliaryDataError extends Data.TaggedError("AuxiliaryDataError")<{ + message?: string + cause?: unknown +}> {} + +/** + * AuxiliaryData based on Conway CDDL specification. + * + * CDDL (Conway era): + * auxiliary_data = { + * ? 0 => metadata ; transaction_metadata + * ? 1 => [* native_script] ; native_scripts + * ? 2 => [* plutus_v1_script] ; plutus_v1_scripts + * ? 3 => [* plutus_v2_script] ; plutus_v2_scripts + * ? 4 => [* plutus_v3_script] ; plutus_v3_scripts + * } + * + * Uses map format with numeric keys as per Conway specification. + * + * @since 2.0.0 + * @category model + */ +export class AuxiliaryData extends Schema.Class("AuxiliaryData")({ + metadata: Schema.optional(Metadata.Metadata), + nativeScripts: Schema.optional(Schema.Array(NativeScripts.Native)), + plutusV1Scripts: Schema.optional(Schema.Array(PlutusV1.PlutusV1)), + plutusV2Scripts: Schema.optional(Schema.Array(PlutusV2.PlutusV2)), + plutusV3Scripts: Schema.optional(Schema.Array(PlutusV3.PlutusV3)) +}) {} + +/** + * CBOR Schema representing auxiliary data as a Conway-tagged map. + * Conway era wraps auxiliary data in CBOR tag 259 (0x103). + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = Schema.MapFromSelf({ + key: CBOR.Integer, + value: Schema.Union( + Metadata.CDDLSchema, + Schema.Array(NativeScripts.CDDLSchema), + Schema.Array(PlutusV1.CDDLSchema), + Schema.Array(PlutusV2.CDDLSchema), + Schema.Array(PlutusV3.CDDLSchema) + ) +}) + +/** + * Transform schema between AuxiliaryData class and Conway-tagged CBOR. + * Uses CBOR tag 259 to wrap auxiliary data for Conway era compatibility. + * + * @since 2.0.0 + * @category schemas + */ +export const FromConwayTagged = Schema.transformOrFail( + Schema.instanceOf(CBOR.Tag), + Schema.typeSchema(AuxiliaryData), + { + strict: true, + encode: (auxData: AuxiliaryData) => + Eff.gen(function* () { + // First encode to CDDL map + const cddlMap = yield* ParseResult.encode(FromCDDL)(auxData) + // Then wrap in Conway tag (259) + return new CBOR.Tag({ tag: 259, value: cddlMap }) + }), + decode: (conwayTag: CBOR.Tag) => + Eff.gen(function* () { + // Extract the map from Conway tag and decode + const cddlMap = conwayTag.value as ReadonlyMap + return yield* ParseResult.decode(FromCDDL)(cddlMap) + }) + } +).annotations({ + identifier: "AuxiliaryData.FromConwayTagged", + title: "AuxiliaryData from Conway tagged CBOR", + description: "Transforms Conway-tagged CBOR to AuxiliaryData" +}) + +/** + * Transform schema between CDDL map representation and AuxiliaryData class. + * Handles Conway-era map format with CBOR tag 259 wrapping. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(AuxiliaryData), { + strict: true, + encode: (toA: AuxiliaryData) => + Eff.gen(function* () { + const result = new Map() + + // Map class properties to CDDL map keys + if (toA.metadata !== undefined) { + const encodedMetadata = yield* ParseResult.encode(Metadata.FromCDDL)(toA.metadata) + result.set(0n, encodedMetadata) + } + + if (toA.nativeScripts !== undefined) { + const encodedNativeScripts = yield* Eff.all( + toA.nativeScripts.map((s) => ParseResult.encode(NativeScripts.FromCDDL)(s)) + ) + result.set(1n, encodedNativeScripts) + } + + if (toA.plutusV1Scripts !== undefined) { + const encodedV1Scripts = yield* Eff.all( + toA.plutusV1Scripts.map((s) => ParseResult.encode(PlutusV1.FromCDDL)(s)) + ) + result.set(2n, encodedV1Scripts) + } + + if (toA.plutusV2Scripts !== undefined) { + const encodedV2Scripts = yield* Eff.all( + toA.plutusV2Scripts.map((s) => ParseResult.encode(PlutusV2.FromCDDL)(s)) + ) + result.set(3n, encodedV2Scripts) + } + + if (toA.plutusV3Scripts !== undefined) { + const encodedV3Scripts = yield* Eff.all( + toA.plutusV3Scripts.map((s) => ParseResult.encode(PlutusV3.FromCDDL)(s)) + ) + result.set(4n, encodedV3Scripts) + } + + return result + }), + decode: (fromA) => + Eff.gen(function* () { + // Extract values from CDDL map and convert to class properties + const metadataValue = fromA.get(0n) as ReadonlyMap | undefined + const metadata = metadataValue ? yield* ParseResult.decode(Metadata.FromCDDL)(metadataValue) : undefined + + const nativeScriptsArray = fromA.get(1n) as ReadonlyArray | undefined + const nativeScripts = nativeScriptsArray + ? yield* Eff.all(nativeScriptsArray.map((s) => ParseResult.decode(NativeScripts.FromCDDL)(s))) + : undefined + + const plutusV1Array = fromA.get(2n) as ReadonlyArray | undefined + const plutusV1Scripts = plutusV1Array + ? yield* Eff.all(plutusV1Array.map((s) => ParseResult.decode(PlutusV1.FromCDDL)(s))) + : undefined + + const plutusV2Array = fromA.get(3n) as ReadonlyArray | undefined + const plutusV2Scripts = plutusV2Array + ? yield* Eff.all(plutusV2Array.map((s) => ParseResult.decode(PlutusV2.FromCDDL)(s))) + : undefined + + const plutusV3Array = fromA.get(4n) as ReadonlyArray | undefined + const plutusV3Scripts = plutusV3Array + ? yield* Eff.all(plutusV3Array.map((s) => ParseResult.decode(PlutusV3.FromCDDL)(s))) + : undefined + + return new AuxiliaryData({ + metadata, + nativeScripts, + plutusV1Scripts, + plutusV2Scripts, + plutusV3Scripts, + }) + }) +}).annotations({ + identifier: "AuxiliaryData.FromCDDL", + title: "AuxiliaryData from CDDL", + description: "Transforms CDDL map representation to AuxiliaryData" +}) + +/** + * CBOR bytes transformation schema for AuxiliaryData. + * Uses Conway tagged format for CML compatibility. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose(CBOR.FromBytes(options), FromConwayTagged).annotations({ + identifier: "AuxiliaryData.FromCBORBytes", + title: "AuxiliaryData from CBOR bytes", + description: "Decode AuxiliaryData from Conway-tagged CBOR-encoded bytes" + }) + +/** + * CBOR hex transformation schema for AuxiliaryData. + * Uses Conway tagged format for CML compatibility. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose(Bytes.FromHex, FromCBORBytes(options)).annotations({ + identifier: "AuxiliaryData.FromCBORHex", + title: "AuxiliaryData from CBOR hex", + description: "Decode AuxiliaryData from Conway-tagged CBOR-encoded hex string" + }) + +/** + * Smart constructor for AuxiliaryData with validation. + * + * @since 2.0.0 + * @category constructors + */ +export const make = AuxiliaryData.make + +/** + * Create an empty AuxiliaryData instance. + * + * @since 2.0.0 + * @category constructors + */ +export const empty = (): AuxiliaryData => new AuxiliaryData({ + metadata: undefined, + nativeScripts: undefined, + plutusV1Scripts: undefined, + plutusV2Scripts: undefined, + plutusV3Scripts: undefined +}) + +/** + * Check if two AuxiliaryData instances are equal (deep comparison). + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: AuxiliaryData, b: AuxiliaryData): boolean => { + if (a.metadata && b.metadata) { + if (!Metadata.equals(a.metadata, b.metadata)) return false + } else if (a.metadata || b.metadata) return false + + const cmpArray = (x?: ReadonlyArray, y?: ReadonlyArray) => + x && y ? x.length === y.length && x.every((v, i) => v === y[i]) : x === y + + if (!cmpArray(a.nativeScripts, b.nativeScripts)) return false + if (!cmpArray(a.plutusV1Scripts, b.plutusV1Scripts)) return false + if (!cmpArray(a.plutusV2Scripts, b.plutusV2Scripts)) return false + if (!cmpArray(a.plutusV3Scripts, b.plutusV3Scripts)) return false + return true +} + +/** + * FastCheck arbitrary for generating random AuxiliaryData instances. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary: FastCheck.Arbitrary = FastCheck.record({ + metadata: FastCheck.option(Metadata.arbitrary, { nil: undefined }), + nativeScripts: FastCheck.option( + FastCheck.array( + // basic native script arbitrary using keyHash sig scripts only for now + FastCheck.record({ + type: FastCheck.constant("sig" as const), + keyHash: FastCheck.hexaString({ minLength: 56, maxLength: 56 }) + }), + { maxLength: 3 } + ), + { nil: undefined } + ), + plutusV1Scripts: FastCheck.option(FastCheck.array(PlutusV1.arbitrary, { maxLength: 3 }), { nil: undefined }), + plutusV2Scripts: FastCheck.option(FastCheck.array(PlutusV2.arbitrary, { maxLength: 3 }), { nil: undefined }), + plutusV3Scripts: FastCheck.option(FastCheck.array(PlutusV3.arbitrary, { maxLength: 3 }), { nil: undefined }) +}).map((r) => new AuxiliaryData(r)) + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Decode AuxiliaryData from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS +): AuxiliaryData => Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Decode AuxiliaryData from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): AuxiliaryData => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +/** + * Encode AuxiliaryData to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (value: AuxiliaryData, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): Uint8Array => + Eff.runSync(Effect.toCBORBytes(value, options)) + +/** + * Encode AuxiliaryData to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (value: AuxiliaryData, options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS): string => + Eff.runSync(Effect.toCBORHex(value, options)) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Decode AuxiliaryData from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new AuxiliaryDataError({ + message: "Failed to decode AuxiliaryData from CBOR bytes", + cause + }) + ) + ) as Eff.Effect + + /** + * Decode AuxiliaryData from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new AuxiliaryDataError({ + message: "Failed to decode AuxiliaryData from CBOR hex", + cause + }) + ) + ) + + /** + * Encode AuxiliaryData to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = ( + value: AuxiliaryData, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORBytes(options))(value).pipe( + Eff.mapError( + (cause) => + new AuxiliaryDataError({ + message: "Failed to encode AuxiliaryData to CBOR bytes", + cause + }) + ) + ) + + /** + * Encode AuxiliaryData to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = ( + value: AuxiliaryData, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORHex(options))(value).pipe( + Eff.mapError( + (cause) => + new AuxiliaryDataError({ + message: "Failed to encode AuxiliaryData to CBOR hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/Certificate.ts b/packages/evolution/src/Certificate.ts index 7b48c7e5..133ec959 100644 --- a/packages/evolution/src/Certificate.ts +++ b/packages/evolution/src/Certificate.ts @@ -21,6 +21,109 @@ export class CertificateError extends Data.TaggedError("CertificateError")<{ cause?: unknown }> {} +export class StakeRegistration extends Schema.TaggedClass("StakeRegistration")("StakeRegistration", { + stakeCredential: Credential.Credential +}) {} + +export class StakeDeregistration extends Schema.TaggedClass("StakeDeregistration")( + "StakeDeregistration", + { + stakeCredential: Credential.Credential + } +) {} + +export class StakeDelegation extends Schema.TaggedClass("StakeDelegation")("StakeDelegation", { + stakeCredential: Credential.Credential, + poolKeyHash: PoolKeyHash.PoolKeyHash +}) {} + +export class PoolRegistration extends Schema.TaggedClass("PoolRegistration")("PoolRegistration", { + poolParams: PoolParams.PoolParams +}) {} + +export class PoolRetirement extends Schema.TaggedClass("PoolRetirement")("PoolRetirement", { + poolKeyHash: PoolKeyHash.PoolKeyHash, + epoch: EpochNo.EpochNoSchema +}) {} + +export class RegCert extends Schema.TaggedClass("RegCert")("RegCert", { + stakeCredential: Credential.Credential, + coin: Coin.Coin +}) {} + +export class UnregCert extends Schema.TaggedClass("UnregCert")("UnregCert", { + stakeCredential: Credential.Credential, + coin: Coin.Coin +}) {} + +export class VoteDelegCert extends Schema.TaggedClass("VoteDelegCert")("VoteDelegCert", { + stakeCredential: Credential.Credential, + drep: DRep.DRep +}) {} + +export class StakeVoteDelegCert extends Schema.TaggedClass("StakeVoteDelegCert")( + "StakeVoteDelegCert", + { + stakeCredential: Credential.Credential, + poolKeyHash: PoolKeyHash.PoolKeyHash, + drep: DRep.DRep + } +) {} + +export class StakeRegDelegCert extends Schema.TaggedClass("StakeRegDelegCert")("StakeRegDelegCert", { + stakeCredential: Credential.Credential, + poolKeyHash: PoolKeyHash.PoolKeyHash, + coin: Coin.Coin +}) {} + +export class VoteRegDelegCert extends Schema.TaggedClass("VoteRegDelegCert")("VoteRegDelegCert", { + stakeCredential: Credential.Credential, + drep: DRep.DRep, + coin: Coin.Coin +}) {} + +export class StakeVoteRegDelegCert extends Schema.TaggedClass("StakeVoteRegDelegCert")( + "StakeVoteRegDelegCert", + { + stakeCredential: Credential.Credential, + poolKeyHash: PoolKeyHash.PoolKeyHash, + drep: DRep.DRep, + coin: Coin.Coin + } +) {} + +export class AuthCommitteeHotCert extends Schema.TaggedClass("AuthCommitteeHotCert")( + "AuthCommitteeHotCert", + { + committeeColdCredential: Credential.Credential, + committeeHotCredential: Credential.Credential + } +) {} + +export class ResignCommitteeColdCert extends Schema.TaggedClass("ResignCommitteeColdCert")( + "ResignCommitteeColdCert", + { + committeeColdCredential: Credential.Credential, + anchor: Schema.NullishOr(Anchor.Anchor) + } +) {} + +export class RegDrepCert extends Schema.TaggedClass("RegDrepCert")("RegDrepCert", { + drepCredential: Credential.Credential, + coin: Coin.Coin, + anchor: Schema.NullishOr(Anchor.Anchor) +}) {} + +export class UnregDrepCert extends Schema.TaggedClass("UnregDrepCert")("UnregDrepCert", { + drepCredential: Credential.Credential, + coin: Coin.Coin +}) {} + +export class UpdateDrepCert extends Schema.TaggedClass("UpdateDrepCert")("UpdateDrepCert", { + drepCredential: Credential.Credential, + anchor: Schema.NullishOr(Anchor.Anchor) +}) {} + /** * Certificate union schema based on Conway CDDL specification * @@ -68,93 +171,39 @@ export class CertificateError extends Data.TaggedError("CertificateError")<{ */ export const Certificate = Schema.Union( // 0: stake_registration = (0, stake_credential) - Schema.TaggedStruct("StakeRegistration", { - stakeCredential: Credential.Credential - }), + StakeRegistration, // 1: stake_deregistration = (1, stake_credential) - Schema.TaggedStruct("StakeDeregistration", { - stakeCredential: Credential.Credential - }), + StakeDeregistration, // 2: stake_delegation = (2, stake_credential, pool_keyhash) - Schema.TaggedStruct("StakeDelegation", { - stakeCredential: Credential.Credential, - poolKeyHash: PoolKeyHash.PoolKeyHash - }), + StakeDelegation, // 3: pool_registration = (3, pool_params) - Schema.TaggedStruct("PoolRegistration", { - poolParams: PoolParams.PoolParams - }), + PoolRegistration, // 4: pool_retirement = (4, pool_keyhash, epoch_no) - Schema.TaggedStruct("PoolRetirement", { - poolKeyHash: PoolKeyHash.PoolKeyHash, - epoch: EpochNo.EpochNoSchema - }), + PoolRetirement, // 7: reg_cert = (7, stake_credential, coin) - Schema.TaggedStruct("RegCert", { - stakeCredential: Credential.Credential, - coin: Coin.Coin - }), + RegCert, // 8: unreg_cert = (8, stake_credential, coin) - Schema.TaggedStruct("UnregCert", { - stakeCredential: Credential.Credential, - coin: Coin.Coin - }), + UnregCert, // 9: vote_deleg_cert = (9, stake_credential, drep) - Schema.TaggedStruct("VoteDelegCert", { - stakeCredential: Credential.Credential, - drep: DRep.DRep - }), + VoteDelegCert, // 10: stake_vote_deleg_cert = (10, stake_credential, pool_keyhash, drep) - Schema.TaggedStruct("StakeVoteDelegCert", { - stakeCredential: Credential.Credential, - poolKeyHash: PoolKeyHash.PoolKeyHash, - drep: DRep.DRep - }), + StakeVoteDelegCert, // 11: stake_reg_deleg_cert = (11, stake_credential, pool_keyhash, coin) - Schema.TaggedStruct("StakeRegDelegCert", { - stakeCredential: Credential.Credential, - poolKeyHash: PoolKeyHash.PoolKeyHash, - coin: Coin.Coin - }), + StakeRegDelegCert, // 12: vote_reg_deleg_cert = (12, stake_credential, drep, coin) - Schema.TaggedStruct("VoteRegDelegCert", { - stakeCredential: Credential.Credential, - drep: DRep.DRep, - coin: Coin.Coin - }), + VoteRegDelegCert, // 13: stake_vote_reg_deleg_cert = (13, stake_credential, pool_keyhash, drep, coin) - Schema.TaggedStruct("StakeVoteRegDelegCert", { - stakeCredential: Credential.Credential, - poolKeyHash: PoolKeyHash.PoolKeyHash, - drep: DRep.DRep, - coin: Coin.Coin - }), + StakeVoteRegDelegCert, // 14: auth_committee_hot_cert = (14, committee_cold_credential, committee_hot_credential) - Schema.TaggedStruct("AuthCommitteeHotCert", { - committeeColdCredential: Credential.Credential, - committeeHotCredential: Credential.Credential - }), + AuthCommitteeHotCert, // 15: resign_committee_cold_cert = (15, committee_cold_credential, anchor/ nil) - Schema.TaggedStruct("ResignCommitteeColdCert", { - committeeColdCredential: Credential.Credential, - anchor: Schema.NullishOr(Anchor.Anchor) - }), + ResignCommitteeColdCert, // 16: reg_drep_cert = (16, drep_credential, coin, anchor/ nil) - Schema.TaggedStruct("RegDrepCert", { - drepCredential: Credential.Credential, - coin: Coin.Coin, - anchor: Schema.NullishOr(Anchor.Anchor) - }), + RegDrepCert, // 17: unreg_drep_cert = (17, drep_credential, coin) - Schema.TaggedStruct("UnregDrepCert", { - drepCredential: Credential.Credential, - coin: Coin.Coin - }), + UnregDrepCert, // 18: update_drep_cert = (18, drep_credential, anchor/ nil) - Schema.TaggedStruct("UpdateDrepCert", { - drepCredential: Credential.Credential, - anchor: Schema.NullishOr(Anchor.Anchor) - }) + UpdateDrepCert ) export const CDDLSchema = Schema.Union( @@ -173,18 +222,9 @@ export const CDDLSchema = Schema.Union( // 8: unreg_cert = (8, stake_credential, coin) Schema.Tuple(Schema.Literal(8n), Credential.CDDLSchema, CBOR.Integer), // 9: vote_deleg_cert = (9, stake_credential, drep) - Schema.Tuple( - Schema.Literal(9n), - Credential.CDDLSchema, - DRep.CDDLSchema - ), + Schema.Tuple(Schema.Literal(9n), Credential.CDDLSchema, DRep.CDDLSchema), // 10: stake_vote_deleg_cert = (10, stake_credential, pool_keyhash, drep) - Schema.Tuple( - Schema.Literal(10n), - Credential.CDDLSchema, - CBOR.ByteArray, - DRep.CDDLSchema, - ), + Schema.Tuple(Schema.Literal(10n), Credential.CDDLSchema, CBOR.ByteArray, DRep.CDDLSchema), // 11: stake_reg_deleg_cert = (11, stake_credential, pool_keyhash, coin) Schema.Tuple(Schema.Literal(11n), Credential.CDDLSchema, CBOR.ByteArray, CBOR.Integer), // 12: vote_reg_deleg_cert = (12, stake_credential, drep, coin) @@ -240,11 +280,11 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Cer } case "RegCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) - return [7n, credentialCDDL, BigInt(toA.coin)] as const + return [7n, credentialCDDL, toA.coin] as const } case "UnregCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) - return [8n, credentialCDDL, BigInt(toA.coin)] as const + return [8n, credentialCDDL, toA.coin] as const } case "VoteDelegCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) @@ -260,18 +300,18 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Cer case "StakeRegDelegCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.poolKeyHash) - return [11n, credentialCDDL, poolKeyHashBytes, BigInt(toA.coin)] as const + return [11n, credentialCDDL, poolKeyHashBytes, toA.coin] as const } case "VoteRegDelegCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) const drepCDDL = yield* ParseResult.encode(DRep.FromCDDL)(toA.drep) - return [12n, credentialCDDL, drepCDDL, BigInt(toA.coin)] as const + return [12n, credentialCDDL, drepCDDL, toA.coin] as const } case "StakeVoteRegDelegCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.stakeCredential) const poolKeyHashBytes = yield* ParseResult.encode(PoolKeyHash.FromBytes)(toA.poolKeyHash) const drepCDDL = yield* ParseResult.encode(DRep.FromCDDL)(toA.drep) - return [13n, credentialCDDL, poolKeyHashBytes, drepCDDL, BigInt(toA.coin)] as const + return [13n, credentialCDDL, poolKeyHashBytes, drepCDDL, toA.coin] as const } case "AuthCommitteeHotCert": { const coldCredentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.committeeColdCredential) @@ -286,11 +326,11 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Cer case "RegDrepCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.drepCredential) const anchorCDDL = toA.anchor ? yield* ParseResult.encode(Anchor.FromCDDL)(toA.anchor) : null - return [16n, credentialCDDL, BigInt(toA.coin), anchorCDDL] as const + return [16n, credentialCDDL, toA.coin, anchorCDDL] as const } case "UnregDrepCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.drepCredential) - return [17n, credentialCDDL, BigInt(toA.coin)] as const + return [17n, credentialCDDL, toA.coin] as const } case "UpdateDrepCert": { const credentialCDDL = yield* ParseResult.encode(Credential.FromCDDL)(toA.drepCredential) @@ -338,7 +378,7 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Cer // pool_retirement = (4, pool_keyhash, epoch_no) const [, poolKeyHashBytes, epochBigInt] = fromA const poolKeyHash = yield* ParseResult.decode(PoolKeyHash.FromBytes)(poolKeyHashBytes) - const epoch = EpochNo.make(Number(epochBigInt)) + const epoch = EpochNo.make(epochBigInt) return yield* ParseResult.decode(Certificate)({ _tag: "PoolRetirement", poolKeyHash, epoch }) } case 7n: { @@ -511,46 +551,67 @@ export const is = Schema.is(Certificate) */ export const arbitrary = FastCheck.oneof( // StakeRegistration - FastCheck.record({ - _tag: FastCheck.constant("StakeRegistration"), - stakeCredential: Credential.arbitrary - }), + Credential.arbitrary.map((stakeCredential) => new StakeRegistration({ stakeCredential })), // StakeDeregistration - FastCheck.record({ - _tag: FastCheck.constant("StakeDeregistration"), - stakeCredential: Credential.arbitrary - }), + Credential.arbitrary.map((stakeCredential) => new StakeDeregistration({ stakeCredential })), // StakeDelegation - FastCheck.record({ - _tag: FastCheck.constant("StakeDelegation"), - stakeCredential: Credential.arbitrary, - poolKeyHash: PoolKeyHash.arbitrary - }), + FastCheck.tuple(Credential.arbitrary, PoolKeyHash.arbitrary).map( + ([stakeCredential, poolKeyHash]) => new StakeDelegation({ stakeCredential, poolKeyHash }) + ), + // PoolRegistration + PoolParams.arbitrary.map((poolParams) => new PoolRegistration({ poolParams })), // PoolRetirement - FastCheck.record({ - _tag: FastCheck.constant("PoolRetirement"), - poolKeyHash: PoolKeyHash.arbitrary, - epoch: EpochNo.generator - }), + FastCheck.tuple(PoolKeyHash.arbitrary, EpochNo.generator).map( + ([poolKeyHash, epoch]) => new PoolRetirement({ poolKeyHash, epoch: EpochNo.make(epoch) }) + ), // RegCert - FastCheck.record({ - _tag: FastCheck.constant("RegCert"), - stakeCredential: Credential.arbitrary, - coin: Coin.arbitrary - }), + FastCheck.tuple(Credential.arbitrary, Coin.arbitrary).map( + ([stakeCredential, coin]) => new RegCert({ stakeCredential, coin }) + ), // UnregCert - FastCheck.record({ - _tag: FastCheck.constant("UnregCert"), - stakeCredential: Credential.arbitrary, - coin: Coin.arbitrary - }), + FastCheck.tuple(Credential.arbitrary, Coin.arbitrary).map( + ([stakeCredential, coin]) => new UnregCert({ stakeCredential, coin }) + ), // VoteDelegCert - FastCheck.record({ - _tag: FastCheck.constant("VoteDelegCert"), - stakeCredential: Credential.arbitrary, - drep: DRep.arbitrary - }) - // Note: Additional certificate types would be added here + FastCheck.tuple(Credential.arbitrary, DRep.arbitrary).map( + ([stakeCredential, drep]) => new VoteDelegCert({ stakeCredential, drep }) + ), + // StakeVoteDelegCert + FastCheck.tuple(Credential.arbitrary, PoolKeyHash.arbitrary, DRep.arbitrary).map( + ([stakeCredential, poolKeyHash, drep]) => new StakeVoteDelegCert({ stakeCredential, poolKeyHash, drep }) + ), + // StakeRegDelegCert + FastCheck.tuple(Credential.arbitrary, PoolKeyHash.arbitrary, Coin.arbitrary).map( + ([stakeCredential, poolKeyHash, coin]) => new StakeRegDelegCert({ stakeCredential, poolKeyHash, coin }) + ), + // VoteRegDelegCert + FastCheck.tuple(Credential.arbitrary, DRep.arbitrary, Coin.arbitrary).map( + ([stakeCredential, drep, coin]) => new VoteRegDelegCert({ stakeCredential, drep, coin }) + ), + // StakeVoteRegDelegCert + FastCheck.tuple(Credential.arbitrary, PoolKeyHash.arbitrary, DRep.arbitrary, Coin.arbitrary).map( + ([stakeCredential, poolKeyHash, drep, coin]) => new StakeVoteRegDelegCert({ stakeCredential, poolKeyHash, drep, coin }) + ), + // AuthCommitteeHotCert + FastCheck.tuple(Credential.arbitrary, Credential.arbitrary).map( + ([committeeColdCredential, committeeHotCredential]) => new AuthCommitteeHotCert({ committeeColdCredential, committeeHotCredential }) + ), + // ResignCommitteeColdCert + FastCheck.tuple(Credential.arbitrary, FastCheck.option(Anchor.arbitrary, { nil: undefined })).map( + ([committeeColdCredential, anchor]) => new ResignCommitteeColdCert({ committeeColdCredential, anchor }) + ), + // RegDrepCert + FastCheck.tuple(Credential.arbitrary, Coin.arbitrary, FastCheck.option(Anchor.arbitrary, { nil: undefined })).map( + ([drepCredential, coin, anchor]) => new RegDrepCert({ drepCredential, coin, anchor }) + ), + // UnregDrepCert + FastCheck.tuple(Credential.arbitrary, Coin.arbitrary).map( + ([drepCredential, coin]) => new UnregDrepCert({ drepCredential, coin }) + ), + // UpdateDrepCert + FastCheck.tuple(Credential.arbitrary, FastCheck.option(Anchor.arbitrary, { nil: undefined })).map( + ([drepCredential, anchor]) => new UpdateDrepCert({ drepCredential, anchor }) + ) ) /** diff --git a/packages/evolution/src/Data.ts b/packages/evolution/src/Data.ts index 91679466..56490514 100644 --- a/packages/evolution/src/Data.ts +++ b/packages/evolution/src/Data.ts @@ -1,4 +1,4 @@ -import { Data as EffectData, Either as E, FastCheck, ParseResult, pipe, Schema } from "effect" +import { Data as EffectData, Effect, Either as E, FastCheck, ParseResult, Schema } from "effect" import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" @@ -684,27 +684,82 @@ export const cborValueToPlutusData = (cborValue: CBOR.CBOR): Data => { }) } +export const CDDLSchema = CBOR.CBORSchema + /** - * CBOR value representation for PlutusData - * This represents the intermediate CBOR structure that corresponds to PlutusData + * CDDL schema for PlutusData following the Conway specification. + * + * ``` + * plutus_data = + * constr + * / {* plutus_data => plutus_data} + * / [* plutus_data] + * / big_int + * / bounded_bytes + * + * constr = + * #6.121([* a0]) // index 0 + * / #6.122([* a0]) // index 1 + * / #6.123([* a0]) // index 2 + * / #6.124([* a0]) // index 3 + * / #6.125([* a0]) // index 4 + * / #6.126([* a0]) // index 5 + * / #6.127([* a0]) // index 6 + * / #6.102([uint, [* a0]]) // general constructor + * + * big_int = int / big_uint / big_nint + * big_uint = #6.2(bounded_bytes) + * big_nint = #6.3(bounded_bytes) + * ``` + * + * This transforms between CBOR values and PlutusData using the existing + * plutusDataToCBORValue and cborValueToPlutusData functions. * * @since 2.0.0 - * @category model + * @category schemas */ +export const FromCDDL = Schema.transformOrFail(CDDLSchema, DataSchema, { + strict: true, + encode: (_, __, ___, data) => Effect.succeed(plutusDataToCBORValue(data)), + decode: (_, __, ___, cborValue) => + Effect.try({ + try: () => cborValueToPlutusData(cborValue), + catch: (error) => new ParseResult.Type(DataSchema.ast, cborValue, String(error)) + }) +}) +/** + * CBOR bytes transformation schema for PlutusData using CDDL. + * Transforms between CBOR bytes and Data using CDDL encoding. + * + * @since 2.0.0 + * @category schemas + */ export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS) => - Schema.transformOrFail(Schema.Uint8ArrayFromSelf, DataSchema, { - strict: true, - encode: (toI) => - pipe(plutusDataToCBORValue(toI), (cborValue) => ParseResult.encode(CBOR.FromBytes(options))(cborValue)), - decode: (fromI) => pipe(ParseResult.decode(CBOR.FromBytes(options))(fromI), ParseResult.map(cborValueToPlutusData)) - }).annotations({ - identifier: "Data.FromCBORBytes" + Schema.compose( + CBOR.FromBytes(options), // Uint8Array → CBOR + FromCDDL // CBOR → Data + ).annotations({ + identifier: "Data.FromCBORBytes", + title: "Data from CBOR Bytes using CDDL", + description: "Transforms CBOR bytes to Data using CDDL encoding" }) +/** + * CBOR hex transformation schema for PlutusData using CDDL. + * Transforms between CBOR hex string and Data using CDDL encoding. + * + * @since 2.0.0 + * @category schemas + */ export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DATA_DEFAULT_OPTIONS) => - Schema.compose(Bytes.FromHex, FromCBORBytes(options)).annotations({ - identifier: "Data.FromCBORHex" + Schema.compose( + Bytes.FromHex, // string → Uint8Array + FromCBORBytes(options) // Uint8Array → Data + ).annotations({ + identifier: "Data.FromCBORHex", + title: "Data from CBOR Hex using CDDL", + description: "Transforms CBOR hex string to Data using CDDL encoding" }) // ============================================================================ diff --git a/packages/evolution/src/Metadata.ts b/packages/evolution/src/Metadata.ts new file mode 100644 index 00000000..18cb3680 --- /dev/null +++ b/packages/evolution/src/Metadata.ts @@ -0,0 +1,397 @@ +import { Data, Effect as Eff, Either, FastCheck, ParseResult, Schema } from "effect" + +import * as CBOR from "./CBOR.js" +import * as Numeric from "./Numeric.js" +import * as TransactionMetadatum from "./TransactionMetadatum.js" + +/** + * Error class for Metadata related operations. + * + * @since 2.0.0 + * @category errors + */ +export class MetadataError extends Data.TaggedError("MetadataError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Type representing a transaction metadatum label (uint .size 8). + * + * @since 2.0.0 + * @category model + */ +export type MetadataLabel = typeof MetadataLabel.Type + +/** + * Schema for transaction metadatum label (uint .size 8). + * + * @since 2.0.0 + * @category schemas + */ +export const MetadataLabel = Numeric.Uint8Schema.annotations({ + identifier: "Metadata.MetadataLabel", + description: "A transaction metadatum label (0-255)" +}) + +/** + * Schema for transaction metadata (map from labels to metadata). + * Represents: metadata = {* transaction_metadatum_label => transaction_metadatum} + * + * @since 2.0.0 + * @category schemas + */ +export const Metadata = Schema.MapFromSelf({ + key: MetadataLabel, + value: TransactionMetadatum.TransactionMetadatum +}).annotations({ + identifier: "Metadata", + description: "Transaction metadata as a map from labels to transaction metadata values" +}) + +export type Metadata = typeof Metadata.Type + +/** + * Schema for CDDL-compatible metadata format. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = Schema.MapFromSelf({ + key: Schema.BigIntFromSelf, + value: TransactionMetadatum.CDDLSchema +}) + +/** + * Transform schema from CDDL to Metadata. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Metadata), { + strict: true, + encode: (toI) => + Either.gen(function* () { + const map = new Map() + for (const [label, metadatum] of toI.entries()) { + const transactionMetadatum = yield* ParseResult.encodeEither(TransactionMetadatum.FromCDDL)(metadatum) + map.set(label, transactionMetadatum) + } + return map + }), + decode: (fromA) => + Either.gen(function* () { + const map = new Map() + for (const [label, metadatum] of fromA.entries()) { + const transactionMetadatum = yield* ParseResult.decodeEither(TransactionMetadatum.FromCDDL)(metadatum) + map.set(label, transactionMetadatum) + } + return map + }) +}) + +/** + * Schema transformer for Metadata from CBOR bytes. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORBytes = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose(CBOR.FromBytes(options), FromCDDL).annotations({ + identifier: "Metadata.FromCBORBytes", + description: "Transforms CBOR bytes to Metadata" + }) + +/** + * Schema transformer for Metadata from CBOR hex string. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose(CBOR.FromHex(options), FromCBORBytes(options)).annotations({ + identifier: "Metadata.FromCBORHex", + description: "Transforms CBOR hex string to Metadata" + }) + +// make + +/** + * Smart constructor for Metadata that validates and applies typing. + * + * @since 2.0.0 + * @category constructors + */ +export const make = (map: Map): Metadata => + Schema.decodeSync(Metadata)(map) + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Check if two Metadata instances are equal. + * + * @since 2.0.0 + * @category utilities + */ +export const equals = (a: Metadata, b: Metadata): boolean => { + if (a.size !== b.size) return false + + for (const [key, value] of a.entries()) { + const bValue = b.get(key) + if (!bValue || !TransactionMetadatum.equals(value, bValue)) return false + } + + return true +} + +/** + * FastCheck arbitrary for generating random Metadata instances. + * + * @since 2.0.0 + * @category testing + */ +export const arbitrary: FastCheck.Arbitrary = FastCheck.array( + FastCheck.tuple( + FastCheck.bigInt({ min: 0n, max: 255n }), // MetadataLabel (uint8) + TransactionMetadatum.arbitrary + ), + { maxLength: 5 } +).map((entries) => fromEntries(entries)) + +// ============================================================================ +// Parsing Functions +// ============================================================================ + +/** + * Parse Metadata from CBOR bytes. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Metadata => + Eff.runSync(Effect.fromCBORBytes(bytes, options)) + +/** + * Parse Metadata from CBOR hex string. + * + * @since 2.0.0 + * @category parsing + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Metadata => + Eff.runSync(Effect.fromCBORHex(hex, options)) + +// ============================================================================ +// Encoding Functions +// ============================================================================ + +/** + * Convert Metadata to CBOR bytes. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORBytes = (metadata: Metadata, options?: CBOR.CodecOptions): Uint8Array => + Eff.runSync(Effect.toCBORBytes(metadata, options)) + +/** + * Convert Metadata to CBOR hex string. + * + * @since 2.0.0 + * @category encoding + */ +export const toCBORHex = (metadata: Metadata, options?: CBOR.CodecOptions): string => + Eff.runSync(Effect.toCBORHex(metadata, options)) + +// ============================================================================ +// Factory Functions +// ============================================================================ + +/** + * Create Metadata from an array of label-metadatum pairs. + * + * @since 2.0.0 + * @category constructors + */ +export const fromEntries = (entries: Array<[MetadataLabel, TransactionMetadatum.TransactionMetadatum]>): Metadata => + Schema.decodeSync(Metadata)(new Map(entries)) + +/** + * Create an empty Metadata map. + * + * @since 2.0.0 + * @category constructors + */ +export const empty = (): Metadata => new Map() as Metadata + +/** + * Add or update a metadata entry. + * + * @since 2.0.0 + * @category constructors + */ +export const set = ( + metadata: Metadata, + label: MetadataLabel, + metadatum: TransactionMetadatum.TransactionMetadatum +): Metadata => { + const newMap = new Map(metadata) + newMap.set(label, metadatum) + return newMap as Metadata +} + +/** + * Get a metadata entry by label. + * + * @since 2.0.0 + * @category utilities + */ +export const get = (metadata: Metadata, label: MetadataLabel): TransactionMetadatum.TransactionMetadatum | undefined => + metadata.get(label) + +/** + * Check if a label exists in the metadata. + * + * @since 2.0.0 + * @category utilities + */ +export const has = (metadata: Metadata, label: MetadataLabel): boolean => metadata.has(label) + +/** + * Remove a metadata entry by label. + * + * @since 2.0.0 + * @category constructors + */ +export const remove = (metadata: Metadata, label: MetadataLabel): Metadata => { + const newMap = new Map(metadata) + newMap.delete(label) + return newMap as Metadata +} + +/** + * Get the size (number of entries) of the metadata. + * + * @since 2.0.0 + * @category utilities + */ +export const size = (metadata: Metadata): number => metadata.size + +/** + * Get all labels in the metadata. + * + * @since 2.0.0 + * @category utilities + */ +export const labels = (metadata: Metadata): Array => Array.from(metadata.keys()) + +/** + * Get all metadata values in the metadata. + * + * @since 2.0.0 + * @category utilities + */ +export const values = (metadata: Metadata): Array => + Array.from(metadata.values()) + +/** + * Get all entries in the metadata. + * + * @since 2.0.0 + * @category utilities + */ +export const entries = (metadata: Metadata): Array<[MetadataLabel, TransactionMetadatum.TransactionMetadatum]> => + Array.from(metadata.entries()) + +// ============================================================================ +// Effect Namespace - Effect-based Error Handling +// ============================================================================ + +/** + * Effect-based error handling variants for functions that can fail. + * + * @since 2.0.0 + * @category effect + */ +export namespace Effect { + /** + * Parse Metadata from CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORBytes = ( + bytes: Uint8Array, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORBytes(options))(bytes).pipe( + Eff.mapError( + (cause) => + new MetadataError({ + message: "Failed to decode Metadata from CBOR bytes", + cause + }) + ) + ) + + /** + * Parse Metadata from CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category parsing + */ + export const fromCBORHex = ( + hex: string, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.decode(FromCBORHex(options))(hex).pipe( + Eff.mapError( + (cause) => + new MetadataError({ + message: "Failed to decode Metadata from CBOR hex", + cause + }) + ) + ) + + /** + * Convert Metadata to CBOR bytes with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORBytes = ( + metadata: Metadata, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORBytes(options))(metadata).pipe( + Eff.mapError( + (cause) => + new MetadataError({ + message: "Failed to encode Metadata to CBOR bytes", + cause + }) + ) + ) + + /** + * Convert Metadata to CBOR hex string with Effect error handling. + * + * @since 2.0.0 + * @category encoding + */ + export const toCBORHex = ( + metadata: Metadata, + options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS + ): Eff.Effect => + Schema.encode(FromCBORHex(options))(metadata).pipe( + Eff.mapError( + (cause) => + new MetadataError({ + message: "Failed to encode Metadata to CBOR hex", + cause + }) + ) + ) +} diff --git a/packages/evolution/src/NativeScripts.ts b/packages/evolution/src/NativeScripts.ts index d32004c4..1dce2298 100644 --- a/packages/evolution/src/NativeScripts.ts +++ b/packages/evolution/src/NativeScripts.ts @@ -1,8 +1,8 @@ import { Data, Effect as Eff, ParseResult, Schema } from "effect" import type { ParseIssue } from "effect/ParseResult" +import * as Bytes from "./Bytes.js" import * as CBOR from "./CBOR.js" -import { Bytes } from "./index.js" /** * Error class for Native script related operations. @@ -116,27 +116,27 @@ export const make = (native: Native): Native => native * @category schemas */ -const ScriptPubKeyCDDL = Schema.Tuple(Schema.Literal(0), Schema.String) +const ScriptPubKeyCDDL = Schema.Tuple(Schema.Literal(0n), Schema.Uint8ArrayFromSelf) const ScriptAllCDDL = Schema.Tuple( - Schema.Literal(1), - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeCDDL))) + Schema.Literal(1n), + Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(FromCDDL))) ) const ScriptAnyCDDL = Schema.Tuple( - Schema.Literal(2), - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeCDDL))) + Schema.Literal(2n), + Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(FromCDDL))) ) const ScriptNOfKCDDL = Schema.Tuple( - Schema.Literal(3), + Schema.Literal(3n), CBOR.Integer, - Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(NativeCDDL))) + Schema.Array(Schema.suspend((): Schema.Schema => Schema.encodedSchema(FromCDDL))) ) -const InvalidBeforeCDDL = Schema.Tuple(Schema.Literal(4), CBOR.Integer) +const InvalidBeforeCDDL = Schema.Tuple(Schema.Literal(4n), CBOR.Integer) -const InvalidHereafterCDDL = Schema.Tuple(Schema.Literal(5), CBOR.Integer) +const InvalidHereafterCDDL = Schema.Tuple(Schema.Literal(5n), CBOR.Integer) /** * CDDL representation of a native script as a union of tuple types. @@ -148,12 +148,21 @@ const InvalidHereafterCDDL = Schema.Tuple(Schema.Literal(5), CBOR.Integer) * @category model */ export type NativeCDDL = - | readonly [0, string] - | readonly [1, ReadonlyArray] - | readonly [2, ReadonlyArray] - | readonly [3, bigint, ReadonlyArray] - | readonly [4, bigint] - | readonly [5, bigint] + | 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. @@ -161,15 +170,11 @@ export type NativeCDDL = * @since 2.0.0 * @category schemas */ -export const NativeCDDL = Schema.transformOrFail( - Schema.Union(ScriptPubKeyCDDL, ScriptAllCDDL, ScriptAnyCDDL, ScriptNOfKCDDL, InvalidBeforeCDDL, InvalidHereafterCDDL), - Schema.typeSchema(Native), - { - strict: true, - encode: (native) => internalEncodeCDDL(native), - decode: (cborTuple) => internalDecodeCDDL(cborTuple) - } -) +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. @@ -181,25 +186,27 @@ export const internalEncodeCDDL = (native: Native): Eff.Effect => Eff.gen(function* () { switch (cborTuple[0]) { - case 0: { - // sig: [0, keyHash_string] - const [, keyHash] = cborTuple + case 0n: { + // sig: [0, keyHash_bytes] - convert bytes back to hex string + const [, keyHashBytes] = cborTuple + const keyHash = yield* ParseResult.encode(Bytes.FromHex)(keyHashBytes) return { type: "sig" as const, keyHash } } - case 1: { + case 1n: { // all: [1, [native_script, ...]] const [, scriptCBORs] = cborTuple const scripts: Array = [] @@ -237,7 +245,7 @@ export const internalDecodeCDDL = (cborTuple: NativeCDDL): Eff.Effect = [] @@ -250,7 +258,7 @@ export const internalDecodeCDDL = (cborTuple: NativeCDDL): Eff.Effect = [] @@ -264,7 +272,7 @@ export const internalDecodeCDDL = (cborTuple: NativeCDDL): Eff.Effect Schema.compose( CBOR.FromBytes(options), // Uint8Array → CBOR - NativeCDDL // CBOR → Native + FromCDDL // CBOR → Native ).annotations({ identifier: "Native.FromCBORBytes", title: "Native from CBOR Bytes", diff --git a/packages/evolution/src/Numeric.ts b/packages/evolution/src/Numeric.ts index 782a156d..9d811d3f 100644 --- a/packages/evolution/src/Numeric.ts +++ b/packages/evolution/src/Numeric.ts @@ -11,8 +11,8 @@ export class NumericError extends Data.TaggedError("NumericError")<{ cause?: unknown }> {} -export const UINT8_MIN = 0 -export const UINT8_MAX = 255 +export const UINT8_MIN = 0n +export const UINT8_MAX = 255n /** * Schema for 8-bit unsigned integers. @@ -20,7 +20,7 @@ export const UINT8_MAX = 255 * @since 2.0.0 * @category schemas */ -export const Uint8Schema = Schema.Number.pipe( +export const Uint8Schema = Schema.BigIntFromSelf.pipe( Schema.filter((number) => Number.isInteger(number) && number >= UINT8_MIN && number <= UINT8_MAX), Schema.annotations({ identifier: "Uint8", @@ -51,7 +51,7 @@ export const Uint8Make = Uint8Schema.make * @since 2.0.0 * @category arbitrary */ -export const Uint8Generator = FastCheck.integer({ +export const Uint8Generator = FastCheck.bigInt({ min: UINT8_MIN, max: UINT8_MAX }).map(Uint8Make) diff --git a/packages/evolution/src/PlutusV1.ts b/packages/evolution/src/PlutusV1.ts new file mode 100644 index 00000000..865bac1c --- /dev/null +++ b/packages/evolution/src/PlutusV1.ts @@ -0,0 +1,75 @@ +import { Data, FastCheck, Schema } from "effect" + +import * as CBOR from "./CBOR.js" + +/** + * Error class for PlutusV1 related operations. + * + * @since 2.0.0 + * @category errors + */ +export class PlutusV1Error extends Data.TaggedError("PlutusV1Error")<{ + message?: string + cause?: unknown +}> {} + +/** + * Plutus V1 script wrapper (raw bytes). + * + * @since 2.0.0 + * @category model + */ +export class PlutusV1 extends Schema.TaggedClass("PlutusV1")("PlutusV1", { + script: Schema.Uint8ArrayFromSelf +}) {} + +/** + * CDDL schema for PlutusV1 scripts as raw bytes. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = CBOR.ByteArray + +/** + * CDDL transformation schema for PlutusV1. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transform(CDDLSchema, PlutusV1, { + strict: true, + encode: (toI) => toI.script, + decode: (fromA) => new PlutusV1({ script: fromA }) +}) + +/** + * Smart constructor for PlutusV1. + * + * @since 2.0.0 + * @category constructors + */ +export const make = PlutusV1.make + +/** + * Check equality of two raw script byte arrays. + */ +const eqBytes = (a: Uint8Array, b: Uint8Array): boolean => a.length === b.length && a.every((v, i) => v === b[i]) + +/** + * Check if two PlutusV1 instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: PlutusV1, b: PlutusV1): boolean => eqBytes(a.script, b.script) + +/** + * FastCheck arbitrary for PlutusV1. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary: FastCheck.Arbitrary = FastCheck.uint8Array({ minLength: 1, maxLength: 512 }).map( + (script) => new PlutusV1({ script }) +) diff --git a/packages/evolution/src/PlutusV2.ts b/packages/evolution/src/PlutusV2.ts new file mode 100644 index 00000000..6b52e255 --- /dev/null +++ b/packages/evolution/src/PlutusV2.ts @@ -0,0 +1,74 @@ +import { Data, FastCheck, Schema } from "effect" + +import * as CBOR from "./CBOR.js" + +/** + * Error class for PlutusV2 related operations. + * + * @since 2.0.0 + * @category errors + */ +export class PlutusV2Error extends Data.TaggedError("PlutusV2Error")<{ + message?: string + cause?: unknown +}> {} + +/** + * Plutus V2 script wrapper (raw bytes). + * + * @since 2.0.0 + * @category model + */ +export class PlutusV2 extends Schema.TaggedClass("PlutusV2")("PlutusV2", { + script: Schema.Uint8ArrayFromSelf +}) {} + +/** + * CDDL schema for PlutusV2 scripts as raw bytes. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = CBOR.ByteArray + +/** + * CDDL transformation schema for PlutusV2. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transform(CDDLSchema, PlutusV2, { + strict: true, + encode: (toI) => toI.script, + decode: (fromA) => new PlutusV2({ script: fromA }) +}) + +/** + * Smart constructor for PlutusV2. + * + * @since 2.0.0 + * @category constructors + */ +export const make = PlutusV2.make + +/** + * Check equality of two raw script byte arrays. + */ +const eqBytes = (a: Uint8Array, b: Uint8Array): boolean => a.length === b.length && a.every((v, i) => v === b[i]) + +/** + * Check if two PlutusV2 instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: PlutusV2, b: PlutusV2): boolean => eqBytes(a.script, b.script) + +/** + * FastCheck arbitrary for PlutusV2. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary: FastCheck.Arbitrary = FastCheck.uint8Array({ minLength: 1, maxLength: 512 }) + .map((script) => new PlutusV2({ script })) diff --git a/packages/evolution/src/PlutusV3.ts b/packages/evolution/src/PlutusV3.ts new file mode 100644 index 00000000..50a8c1a7 --- /dev/null +++ b/packages/evolution/src/PlutusV3.ts @@ -0,0 +1,74 @@ +import { Data, FastCheck, Schema } from "effect" + +import * as CBOR from "./CBOR.js" + +/** + * Error class for PlutusV3 related operations. + * + * @since 2.0.0 + * @category errors + */ +export class PlutusV3Error extends Data.TaggedError("PlutusV3Error")<{ + message?: string + cause?: unknown +}> {} + +/** + * Plutus V3 script wrapper (raw bytes). + * + * @since 2.0.0 + * @category model + */ +export class PlutusV3 extends Schema.TaggedClass("PlutusV3")("PlutusV3", { + script: Schema.Uint8ArrayFromSelf +}) {} + +/** + * CDDL schema for PlutusV3 scripts as raw bytes. + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = CBOR.ByteArray + +/** + * CDDL transformation schema for PlutusV3. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transform(CDDLSchema, PlutusV3, { + strict: true, + encode: (toI) => toI.script, + decode: (fromA) => new PlutusV3({ script: fromA }) +}) + +/** + * Smart constructor for PlutusV3. + * + * @since 2.0.0 + * @category constructors + */ +export const make = PlutusV3.make + +/** + * Check equality of two raw script byte arrays. + */ +const eqBytes = (a: Uint8Array, b: Uint8Array): boolean => a.length === b.length && a.every((v, i) => v === b[i]) + +/** + * Check if two PlutusV3 instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: PlutusV3, b: PlutusV3): boolean => eqBytes(a.script, b.script) + +/** + * FastCheck arbitrary for PlutusV3. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary: FastCheck.Arbitrary = FastCheck.uint8Array({ minLength: 1, maxLength: 512 }) + .map((script) => new PlutusV3({ script })) diff --git a/packages/evolution/src/Redeemer.ts b/packages/evolution/src/Redeemer.ts new file mode 100644 index 00000000..85af2d88 --- /dev/null +++ b/packages/evolution/src/Redeemer.ts @@ -0,0 +1,415 @@ +import { Data, Effect, FastCheck, ParseResult, Schema } from "effect" + +import * as Bytes from "./Bytes.js" +import * as CBOR from "./CBOR.js" +import * as PlutusData from "./Data.js" +import * as Numeric from "./Numeric.js" + +/** + * Error class for Redeemer related operations. + * + * @since 2.0.0 + * @category errors + */ +export class RedeemerError extends Data.TaggedError("RedeemerError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Redeemer tag enum for different script execution contexts. + * + * CDDL: redeemer_tag = 0 ; spend | 1 ; mint | 2 ; cert | 3 ; reward + * + * @since 2.0.0 + * @category model + */ +export const RedeemerTag = Schema.Literal("spend", "mint", "cert", "reward").annotations({ + identifier: "Redeemer.Tag", + title: "Redeemer Tag", + description: "Tag indicating the context where the redeemer is used" +}) + +export type RedeemerTag = typeof RedeemerTag.Type + +/** + * Execution units for Plutus script execution. + * + * CDDL: ex_units = [mem: uint64, steps: uint64] + * + * @since 2.0.0 + * @category model + */ +export const ExUnits = Schema.Tuple( + Numeric.Uint64Schema.annotations({ + identifier: "Redeemer.ExUnits.Memory", + title: "Memory Units", + description: "Memory units consumed by script execution" + }), + Numeric.Uint64Schema.annotations({ + identifier: "Redeemer.ExUnits.Steps", + title: "CPU Steps", + description: "CPU steps consumed by script execution" + }) +).annotations({ + identifier: "Redeemer.ExUnits", + title: "Execution Units", + description: "Memory and CPU limits for Plutus script execution" +}) + +export type ExUnits = typeof ExUnits.Type + +/** + * Redeemer for Plutus script execution based on Conway CDDL specification. + * + * CDDL: redeemer = [ tag, index, data, ex_units ] + * Where: + * - tag: redeemer_tag (0=spend, 1=mint, 2=cert, 3=reward) + * - index: uint64 (index into the respective input/output/certificate/reward array) + * - data: plutus_data (the actual redeemer data passed to the script) + * - ex_units: [mem: uint64, steps: uint64] (execution unit limits) + * + * @since 2.0.0 + * @category model + */ +export class Redeemer extends Schema.Class("Redeemer")({ + tag: RedeemerTag, + index: Numeric.Uint64Schema.annotations({ + identifier: "Redeemer.Index", + title: "Redeemer Index", + description: "Index into the respective transaction array (inputs, outputs, certificates, or rewards)" + }), + data: PlutusData.DataSchema.annotations({ + identifier: "Redeemer.Data", + title: "Redeemer Data", + description: "PlutusData passed to the script for validation" + }), + exUnits: ExUnits +}) {} + +/** + * Helper function to convert RedeemerTag string to CBOR integer. + * + * @since 2.0.0 + * @category utilities + */ +export const tagToInteger = (tag: RedeemerTag): bigint => { + switch (tag) { + case "spend": + return 0n + case "mint": + return 1n + case "cert": + return 2n + case "reward": + return 3n + } +} + +/** + * Helper function to convert CBOR integer to RedeemerTag string. + * + * @since 2.0.0 + * @category utilities + */ +export const integerToTag = (value: bigint): RedeemerTag => { + switch (value) { + case 0n: + return "spend" + case 1n: + return "mint" + case 2n: + return "cert" + case 3n: + return "reward" + default: + throw new RedeemerError({ + message: `Invalid redeemer tag: ${value}. Must be 0 (spend), 1 (mint), 2 (cert), or 3 (reward)` + }) + } +} + +/** + * CDDL schema for Redeemer as tuple structure. + * + * CDDL: redeemer = [ tag, index, data, ex_units ] + * + * @since 2.0.0 + * @category schemas + */ +export const CDDLSchema = Schema.Tuple( + CBOR.Integer.annotations({ + identifier: "Redeemer.CDDL.Tag", + title: "Redeemer Tag (CBOR)", + description: "Redeemer tag as CBOR integer (0=spend, 1=mint, 2=cert, 3=reward)" + }), + CBOR.Integer.annotations({ + identifier: "Redeemer.CDDL.Index", + title: "Redeemer Index (CBOR)", + description: "Index into transaction array as CBOR integer" + }), + PlutusData.CDDLSchema.annotations({ + identifier: "Redeemer.CDDL.Data", + title: "Redeemer Data (CBOR)", + description: "PlutusData as CBOR value" + }), + Schema.Tuple(CBOR.Integer, CBOR.Integer).annotations({ + identifier: "Redeemer.CDDL.ExUnits", + title: "Execution Units (CBOR)", + description: "Memory and CPU limits as CBOR integers" + }) +).annotations({ + identifier: "Redeemer.CDDLSchema", + title: "Redeemer CDDL Schema", + description: "CDDL representation of Redeemer as tuple" +}) + +/** + * CDDL transformation schema for Redeemer. + * + * Transforms between CBOR tuple representation and Redeemer class instance. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Redeemer), { + strict: true, + encode: (redeemer) => + Effect.gen(function* () { + const tagInteger = tagToInteger(redeemer.tag) + const dataCBOR = yield* ParseResult.encode(PlutusData.FromCDDL)(redeemer.data) + return [tagInteger, redeemer.index, dataCBOR, redeemer.exUnits] as const + }), + decode: ([tagInteger, index, dataCBOR, exUnits]) => + Effect.gen(function* () { + const tag = yield* Effect.try({ + try: () => integerToTag(tagInteger), + catch: (error) => new ParseResult.Type(RedeemerTag.ast, tagInteger, String(error)) + }) + const data = yield* ParseResult.decode(PlutusData.FromCDDL)(dataCBOR) + return new Redeemer({ tag, index, data, exUnits }) + }) +}) + +/** + * CBOR bytes transformation schema for Redeemer using CDDL. + * Transforms between CBOR bytes and Redeemer using CDDL 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 → Redeemer + ).annotations({ + identifier: "Redeemer.FromCBORBytes", + title: "Redeemer from CBOR Bytes using CDDL", + description: "Transforms CBOR bytes to Redeemer using CDDL encoding" + }) + +/** + * CBOR hex transformation schema for Redeemer using CDDL. + * Transforms between CBOR hex string and Redeemer using CDDL encoding. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCBORHex = (options: CBOR.CodecOptions = CBOR.CML_DEFAULT_OPTIONS) => + Schema.compose( + Bytes.FromHex, // string → Uint8Array + FromCBORBytes(options) // Uint8Array → Redeemer + ).annotations({ + identifier: "Redeemer.FromCBORHex", + title: "Redeemer from CBOR Hex using CDDL", + description: "Transforms CBOR hex string to Redeemer using CDDL encoding" + }) + +// ============================================================================ +// Constructors +// ============================================================================ + +/** + * Create a spend redeemer for spending UTxO inputs. + * + * @since 2.0.0 + * @category constructors + */ +export const spend = (index: bigint, data: PlutusData.Data, exUnits: ExUnits): Redeemer => + new Redeemer({ tag: "spend", index, data, exUnits }) + +/** + * Create a mint redeemer for minting/burning tokens. + * + * @since 2.0.0 + * @category constructors + */ +export const mint = (index: bigint, data: PlutusData.Data, exUnits: ExUnits): Redeemer => + new Redeemer({ tag: "mint", index, data, exUnits }) + +/** + * Create a cert redeemer for certificate validation. + * + * @since 2.0.0 + * @category constructors + */ +export const cert = (index: bigint, data: PlutusData.Data, exUnits: ExUnits): Redeemer => + new Redeemer({ tag: "cert", index, data, exUnits }) + +/** + * Create a reward redeemer for withdrawal validation. + * + * @since 2.0.0 + * @category constructors + */ +export const reward = (index: bigint, data: PlutusData.Data, exUnits: ExUnits): Redeemer => + new Redeemer({ tag: "reward", index, data, exUnits }) + +// ============================================================================ +// Utilities +// ============================================================================ + +/** + * Check if a redeemer is for spending inputs. + * + * @since 2.0.0 + * @category predicates + */ +export const isSpend = (redeemer: Redeemer): boolean => redeemer.tag === "spend" + +/** + * Check if a redeemer is for minting/burning. + * + * @since 2.0.0 + * @category predicates + */ +export const isMint = (redeemer: Redeemer): boolean => redeemer.tag === "mint" + +/** + * Check if a redeemer is for certificates. + * + * @since 2.0.0 + * @category predicates + */ +export const isCert = (redeemer: Redeemer): boolean => redeemer.tag === "cert" + +/** + * Check if a redeemer is for withdrawals. + * + * @since 2.0.0 + * @category predicates + */ +export const isReward = (redeemer: Redeemer): boolean => redeemer.tag === "reward" + +// ============================================================================ +// Transformations +// ============================================================================ + +/** + * Encode Redeemer to CBOR bytes. + * + * @since 2.0.0 + * @category transformation + */ +export const toCBORBytes = (redeemer: Redeemer, options?: CBOR.CodecOptions): Uint8Array => { + try { + return Schema.encodeSync(FromCBORBytes(options))(redeemer) + } catch (cause) { + throw new RedeemerError({ + message: "Failed to encode Redeemer to CBOR bytes", + cause + }) + } +} + +/** + * Encode Redeemer to CBOR hex string. + * + * @since 2.0.0 + * @category transformation + */ +export const toCBORHex = (redeemer: Redeemer, options?: CBOR.CodecOptions): string => { + try { + return Schema.encodeSync(FromCBORHex(options))(redeemer) + } catch (cause) { + throw new RedeemerError({ + message: "Failed to encode Redeemer to CBOR hex", + cause + }) + } +} + +/** + * Decode Redeemer from CBOR bytes. + * + * @since 2.0.0 + * @category transformation + */ +export const fromCBORBytes = (bytes: Uint8Array, options?: CBOR.CodecOptions): Redeemer => { + try { + return Schema.decodeSync(FromCBORBytes(options))(bytes) + } catch (cause) { + throw new RedeemerError({ + message: "Failed to decode Redeemer from CBOR bytes", + cause + }) + } +} + +/** + * Decode Redeemer from CBOR hex string. + * + * @since 2.0.0 + * @category transformation + */ +export const fromCBORHex = (hex: string, options?: CBOR.CodecOptions): Redeemer => { + try { + return Schema.decodeSync(FromCBORHex(options))(hex) + } catch (cause) { + throw new RedeemerError({ + message: "Failed to decode Redeemer from CBOR hex", + cause + }) + } +} + +// ============================================================================ +// Generators +// ============================================================================ + +/** + * FastCheck arbitrary for generating random RedeemerTag values. + * + * @since 2.0.0 + * @category generators + */ +export const arbitraryRedeemerTag: FastCheck.Arbitrary = FastCheck.constantFrom( + "spend", + "mint", + "cert", + "reward" +) + +/** + * FastCheck arbitrary for generating random ExUnits values. + * + * @since 2.0.0 + * @category generators + */ +export const arbitraryExUnits: FastCheck.Arbitrary = FastCheck.tuple( + FastCheck.bigInt({ min: 0n, max: 10_000_000n }), // memory + FastCheck.bigInt({ min: 0n, max: 10_000_000n }) // steps +) + +/** + * FastCheck arbitrary for generating random Redeemer instances. + * + * @since 2.0.0 + * @category generators + */ +export const arbitrary: FastCheck.Arbitrary = FastCheck.record({ + index: FastCheck.bigInt({ min: 0n, max: 1000n }), + tag: arbitraryRedeemerTag, + data: PlutusData.arbitrary, + exUnits: arbitraryExUnits +}).map(({ data, exUnits, index, tag }) => new Redeemer({ tag, index, data, exUnits })) diff --git a/packages/evolution/src/Script.ts b/packages/evolution/src/Script.ts new file mode 100644 index 00000000..fcdb4c43 --- /dev/null +++ b/packages/evolution/src/Script.ts @@ -0,0 +1,175 @@ +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" + +import * as CBOR from "./CBOR.js" +import * as NativeScripts from "./NativeScripts.js" +import * as PlutusV1 from "./PlutusV1.js" +import * as PlutusV2 from "./PlutusV2.js" +import * as PlutusV3 from "./PlutusV3.js" + +/** + * Error class for Script related operations. + * + * @since 2.0.0 + * @category errors + */ +export class ScriptError extends Data.TaggedError("ScriptError")<{ + message?: string + cause?: unknown +}> {} + +/** + * Script union type following Conway CDDL specification. + * + * CDDL: + * script = + * [ 0, native_script ] + * / [ 1, plutus_v1_script ] + * / [ 2, plutus_v2_script ] + * / [ 3, plutus_v3_script ] + * + * @since 2.0.0 + * @category model + */ +export const Script = Schema.Union( + NativeScripts.Native, + PlutusV1.PlutusV1, + PlutusV2.PlutusV2, + PlutusV3.PlutusV3 +).annotations({ + identifier: "Script", + description: "Script union (native | plutus_v1 | plutus_v2 | plutus_v3)" +}) + +export type Script = typeof Script.Type + +/** + * CDDL schema for Script as tagged tuples. + * + * @since 2.0.0 + * @category schemas + */ +export const ScriptCDDL = Schema.Union( + Schema.Tuple(Schema.Literal(0n), NativeScripts.CDDLSchema), + Schema.Tuple(Schema.Literal(1n), CBOR.ByteArray), // plutus_v1_script + Schema.Tuple(Schema.Literal(2n), CBOR.ByteArray), // plutus_v2_script + Schema.Tuple(Schema.Literal(3n), CBOR.ByteArray) // plutus_v3_script +).annotations({ + identifier: "Script.CDDL", + description: "CDDL representation of Script as tagged tuples" +}) + +export type ScriptCDDL = typeof ScriptCDDL.Type + +/** + * Transformation between CDDL representation and Script union. + * + * @since 2.0.0 + * @category schemas + */ +export const FromCDDL = Schema.transformOrFail( + ScriptCDDL, + Script, + { + strict: true, + encode: (value, _, ast) => { + // Handle native scripts (no _tag property, has type property) + if ("type" in value) { + return NativeScripts.internalEncodeCDDL(value as NativeScripts.Native).pipe( + Eff.map((nativeCDDL) => [0n, nativeCDDL] as const), + Eff.mapError((cause) => new ParseResult.Type(ast, value, `Failed to encode native script: ${cause}`)) + ) + } + + // Handle Plutus scripts (with _tag property) + if ("_tag" in value) { + const plutusScript = value as PlutusV1.PlutusV1 | PlutusV2.PlutusV2 | PlutusV3.PlutusV3 + switch (plutusScript._tag) { + case "PlutusV1": + return Eff.succeed([1n, plutusScript.script] as const) + case "PlutusV2": + return Eff.succeed([2n, plutusScript.script] as const) + case "PlutusV3": + return Eff.succeed([3n, plutusScript.script] as const) + default: + return Eff.fail(new ParseResult.Type(ast, value, `Unknown Plutus script type: ${(plutusScript as any)._tag}`)) + } + } + + return Eff.fail(new ParseResult.Type(ast, value, "Invalid script structure")) + }, + decode: (tuple, _, ast) => { + const [tag, data] = tuple + switch (tag) { + case 0n: + // Native script + return NativeScripts.internalDecodeCDDL(data as NativeScripts.NativeCDDL).pipe( + Eff.mapError((cause) => new ParseResult.Type(ast, tuple, `Failed to decode native script: ${cause}`)) + ) + case 1n: + // PlutusV1 + return Eff.succeed(new PlutusV1.PlutusV1({ script: data as Uint8Array })) + case 2n: + // PlutusV2 + return Eff.succeed(new PlutusV2.PlutusV2({ script: data as Uint8Array })) + case 3n: + // PlutusV3 + return Eff.succeed(new PlutusV3.PlutusV3({ script: data as Uint8Array })) + default: + return Eff.fail(new ParseResult.Type(ast, tuple, `Unknown script tag: ${tag}`)) + } + } + } +).annotations({ + identifier: "Script.FromCDDL", + title: "Script from CDDL", + description: "Transforms between CDDL tagged tuple and Script union" +}) + +/** + * Check if two Script instances are equal. + * + * @since 2.0.0 + * @category equality + */ +export const equals = (a: Script, b: Script): boolean => { + // Handle native scripts (no _tag property, has type property) + if ("type" in a && "type" in b) { + // Simple JSON comparison for native scripts + return JSON.stringify(a) === JSON.stringify(b) + } + + // Handle Plutus scripts (with _tag property) + if ("_tag" in a && "_tag" in b) { + if (a._tag !== b._tag) return false + + switch (a._tag) { + case "PlutusV1": + return PlutusV1.equals(a, b as PlutusV1.PlutusV1) + case "PlutusV2": + return PlutusV2.equals(a, b as PlutusV2.PlutusV2) + case "PlutusV3": + return PlutusV3.equals(a, b as PlutusV3.PlutusV3) + default: + return a === b + } + } + + return false +} + +/** + * FastCheck arbitrary for Script. + * + * @since 2.0.0 + * @category arbitrary + */ +export const arbitrary: FastCheck.Arbitrary