Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/evolution/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"typescript": "^5.9.2"
},
"dependencies": {
"@effect/platform": "^0.90.6",
"@effect/platform-node": "^0.96.0",
"@noble/hashes": "^1.8.0",
"@scure/base": "^1.2.6",
Expand Down
22 changes: 11 additions & 11 deletions packages/evolution/src/AddressDetails.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Data, Effect as Eff, ParseResult, Schema } from "effect"

import * as Address from "./Address.js"
import * as AddressEras from "./AddressEras.js"
import * as Bytes from "./Bytes.js"
import * as Function from "./Function.js"
import * as NetworkId from "./NetworkId.js"
Expand All @@ -26,7 +26,7 @@ export class AddressDetails extends Schema.Class<AddressDetails>("AddressDetails
Schema.Literal("RewardAccount"),
Schema.Literal("ByronAddress")
),
address: Address.Address,
address: AddressEras.AddressEras,
bech32: Schema.String,
hex: Bytes.HexSchema
}) {}
Expand All @@ -36,8 +36,8 @@ export const FromBech32 = Schema.transformOrFail(Schema.String, AddressDetails,
encode: (_, __, ___, toA) => ParseResult.succeed(toA.bech32),
decode: (_, __, ___, fromA) =>
Eff.gen(function* () {
const address = yield* ParseResult.decode(Address.FromBech32)(fromA)
const hex = yield* ParseResult.encode(Address.FromHex)(address)
const address = yield* ParseResult.decode(AddressEras.FromBech32)(fromA)
const hex = yield* ParseResult.encode(AddressEras.FromHex)(address)
return new AddressDetails({
networkId: address.networkId,
type: address._tag,
Expand All @@ -53,8 +53,8 @@ export const FromHex = Schema.transformOrFail(Bytes.HexSchema, AddressDetails, {
encode: (_, __, ___, toA) => ParseResult.succeed(toA.hex),
decode: (_, __, ___, fromA) =>
Eff.gen(function* () {
const address = yield* ParseResult.decode(Address.FromHex)(fromA)
const bech32 = yield* ParseResult.encode(Address.FromBech32)(address)
const address = yield* ParseResult.decode(AddressEras.FromHex)(fromA)
const bech32 = yield* ParseResult.encode(AddressEras.FromBech32)(address)
return new AddressDetails({
networkId: address.networkId,
type: address._tag,
Expand Down Expand Up @@ -83,7 +83,7 @@ export const equals = (self: AddressDetails, that: AddressDetails): boolean => {
return (
self.networkId === that.networkId &&
self.type === that.type &&
Address.equals(self.address, that.address) &&
AddressEras.equals(self.address, that.address) &&
self.bech32 === that.bech32 &&
self.hex === that.hex
)
Expand All @@ -95,18 +95,18 @@ export const equals = (self: AddressDetails, that: AddressDetails): boolean => {
* @since 2.0.0
* @category arbitrary
*/
export const arbitrary = Address.arbitrary.map((address) => fromAddress(address))
export const arbitrary = AddressEras.arbitrary.map((address) => fromAddress(address))

/**
* Create AddressDetails from an Address.
*
* @since 2.0.0
* @category constructors
*/
export const fromAddress = (address: Address.Address): AddressDetails => {
export const fromAddress = (address: AddressEras.AddressEras): 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))
const bech32 = Eff.runSync(Schema.encode(AddressEras.FromBech32)(address))
const hex = Eff.runSync(Schema.encode(AddressEras.FromHex)(address))
return new AddressDetails({
networkId: address.networkId,
type: address._tag,
Expand Down
313 changes: 313 additions & 0 deletions packages/evolution/src/AddressEras.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
import { bech32 } from "@scure/base"
import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect"

import * as BaseAddress from "./BaseAddress.js"
import * as ByronAddress from "./ByronAddress.js"
import * as Bytes from "./Bytes.js"
import * as EnterpriseAddress from "./EnterpriseAddress.js"
import * as Function from "./Function.js"
import * as PointerAddress from "./PointerAddress.js"
import * as RewardAccount from "./RewardAccount.js"

/**
* CDDL specs
* ```
* ; address format:
* ; [ 8 bit header | payload ];
* ;
* ; shelley payment addresses:
* ; bit 7: 0
* ; bit 6: base/other
* ; bit 5: pointer/enterprise [for base: stake cred is keyhash/scripthash]
* ; bit 4: payment cred is keyhash/scripthash
* ; bits 3-0: network id
* ;
* ; reward addresses:
* ; bits 7-5: 111
* ; bit 4: credential is keyhash/scripthash
* ; bits 3-0: network id
* ;
* ; byron addresses:
* ; bits 7-4: 1000
* ;
* ; 0000: base address: keyhash28,keyhash28
* ; 0001: base address: scripthash28,keyhash28
* ; 0010: base address: keyhash28,scripthash28
* ; 0011: base address: scripthash28,scripthash28
* ; 0100: pointer address: keyhash28, 3 variable length uint
* ; 0101: pointer address: scripthash28, 3 variable length uint
* ; 0110: enterprise address: keyhash28
* ; 0111: enterprise address: scripthash28
* ; 1000: byron address
* ; 1110: reward account: keyhash28
* ; 1111: reward account: scripthash28
* ; 1001-1101: future formats
* ```
*/

/**
* Error thrown when address operations fail
*
* @since 2.0.0
* @category model
*/
export class AddressError extends Data.TaggedError("AddressError")<{
message?: string
cause?: unknown
}> {}

/**
* Union type representing all possible address types.
*
* @since 2.0.0
* @category model
*/
export const AddressEras = Schema.Union(
BaseAddress.BaseAddress,
EnterpriseAddress.EnterpriseAddress,
PointerAddress.PointerAddress,
RewardAccount.RewardAccount,
ByronAddress.ByronAddress
)

export const isAddress = Schema.is(AddressEras)

/**
* Type representing an address.
*
* @since 2.0.0
* @category model
*/
export type AddressEras = typeof AddressEras.Type

/**
* Schema for encoding/decoding addresses as bytes.
*
* @since 2.0.0
* @category schema
*/
export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, AddressEras, {
strict: true,
encode: (_, __, ___, toA) => {
switch (toA._tag) {
case "BaseAddress":
return ParseResult.encode(BaseAddress.FromBytes)(toA)
case "EnterpriseAddress":
return ParseResult.encode(EnterpriseAddress.FromBytes)(toA)
case "PointerAddress":
return ParseResult.encode(PointerAddress.FromBytes)(toA)
case "RewardAccount":
return ParseResult.encode(RewardAccount.FromBytes)(toA)
case "ByronAddress":
return ParseResult.encode(ByronAddress.BytesSchema)(toA)
}
},
decode: (_, __, ast, fromA) =>
Eff.gen(function* () {
const header = fromA[0]
// Extract address type from the upper 4 bits (bits 4-7)
const addressType = header >> 4

switch (addressType) {
// Base address types (0000, 0001, 0010, 0011)
// Format: [payment credential, stake credential]
case 0b0000: // Key payment, Key stake
case 0b0001: // Script payment, Key stake
case 0b0010: // Key payment, Script stake
case 0b0011:
return yield* ParseResult.decode(BaseAddress.FromBytes)(fromA)

// Enterprise address types (0110, 0111)
// Format: [payment credential only]
case 0b0110: // Key payment
case 0b0111:
return yield* ParseResult.decode(EnterpriseAddress.FromBytes)(fromA)

// Pointer address types (0100, 0101)
// Format: [payment credential, variable length integers for slot, txIndex, certIndex]
case 0b0100: // Key payment with pointer
case 0b0101:
return yield* ParseResult.decode(PointerAddress.FromBytes)(fromA)

case 0b1110:
case 0b1111:
return yield* ParseResult.decode(RewardAccount.FromBytes)(fromA)

case 0b1000:
return yield* ParseResult.decode(ByronAddress.BytesSchema)(fromA)

default:
return yield* ParseResult.fail(new ParseResult.Type(ast, fromA, `Unknown address type: ${addressType}`))
}
})
})

/**
* Schema for encoding/decoding addresses as hex strings.
*
* @since 2.0.0
* @category schema
*/
export const FromHex = Schema.compose(Bytes.FromHex, FromBytes)

/**
* Schema for encoding/decoding addresses as Bech32 strings.
*
* @since 2.0.0
* @category schema
*/
export const FromBech32 = Schema.transformOrFail(Schema.String, AddressEras, {
strict: true,
encode: (_, __, ast, toA) =>
Eff.gen(function* () {
const bytes = yield* ParseResult.encode(FromBytes)(toA)
let prefix: string
switch (toA._tag) {
case "BaseAddress":
case "EnterpriseAddress":
case "PointerAddress":
prefix = toA.networkId === 0 ? "addr_test" : "addr"
break
case "RewardAccount":
prefix = toA.networkId === 0 ? "stake_test" : "stake"
break
case "ByronAddress":
return yield* ParseResult.fail(
new ParseResult.Type(ast, toA, "Byron addresses do not support Bech32 encoding")
)
}
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: (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"
})

/**
* Checks if two addresses are equal.
*
* @since 2.0.0
* @category utils
*/
export const equals = (a: AddressEras, b: AddressEras): boolean => {
if (a._tag !== b._tag) {
return false
}
switch (a._tag) {
case "BaseAddress":
return BaseAddress.equals(a, b as BaseAddress.BaseAddress)
case "EnterpriseAddress":
return EnterpriseAddress.equals(a, b as EnterpriseAddress.EnterpriseAddress)
case "PointerAddress":
return PointerAddress.equals(a, b as PointerAddress.PointerAddress)
case "RewardAccount":
return RewardAccount.equals(a, b as RewardAccount.RewardAccount)
case "ByronAddress":
return false
}
}

/**
* FastCheck arbitrary for Address instances.
*
* @since 2.0.0
* @category arbitrary
*
*/
export const arbitrary = FastCheck.oneof(
BaseAddress.arbitrary,
EnterpriseAddress.arbitrary,
PointerAddress.arbitrary,
RewardAccount.arbitrary
)

// ============================================================================
// Parsing Functions
// ============================================================================

/**
* Parse an Address from bytes.
*
* @since 2.0.0
* @category parsing
*/
export const fromBytes = Function.makeDecodeSync(FromBytes, AddressError, "Address.fromBytes")

/**
* Parse an Address from hex string.
*
* @since 2.0.0
* @category parsing
*/
export const fromHex = Function.makeDecodeSync(FromHex, AddressError, "Address.fromHex")

/**
* Parse an Address from Bech32 string.
*
* @since 2.0.0
* @category parsing
*/
export const fromBech32 = Function.makeDecodeSync(FromBech32, AddressError, "Address.fromBech32")

// ============================================================================
// Encoding Functions
// ============================================================================

/**
* Convert an Address to bytes.
*
* @since 2.0.0
* @category encoding
*/
export const toBytes = Function.makeEncodeSync(FromBytes, AddressError, "Address.toBytes")

/**
* Convert an Address to hex string.
*
* @since 2.0.0
* @category encoding
*/
export const toHex = Function.makeEncodeSync(FromHex, AddressError, "Address.toHex")

/**
* Convert an Address to Bech32 string.
*
* @since 2.0.0
* @category encoding
*/
export const toBech32 = Function.makeEncodeSync(FromBech32, AddressError, "Address.toBech32")

/**
* Effect-based error handling variants for functions that can fail.
*
* @since 2.0.0
* @category effect
*/
export namespace Either {
export const fromBytes = Function.makeDecodeEither(FromBytes, AddressError)
export const fromHex = Function.makeDecodeEither(FromHex, AddressError)
export const fromBech32 = Function.makeDecodeEither(FromBech32, AddressError)

export const toBytes = Function.makeEncodeEither(FromBytes, AddressError)
export const toHex = Function.makeEncodeEither(FromHex, AddressError)
export const toBech32 = Function.makeEncodeEither(FromBech32, AddressError)
}
Loading
Loading