|
| 1 | +import { bech32 } from "@scure/base" |
| 2 | +import { Data, Effect as Eff, FastCheck, ParseResult, Schema } from "effect" |
| 3 | + |
| 4 | +import * as BaseAddress from "./BaseAddress.js" |
| 5 | +import * as ByronAddress from "./ByronAddress.js" |
| 6 | +import * as Bytes from "./Bytes.js" |
| 7 | +import * as EnterpriseAddress from "./EnterpriseAddress.js" |
| 8 | +import * as Function from "./Function.js" |
| 9 | +import * as PointerAddress from "./PointerAddress.js" |
| 10 | +import * as RewardAccount from "./RewardAccount.js" |
| 11 | + |
| 12 | +/** |
| 13 | + * CDDL specs |
| 14 | + * ``` |
| 15 | + * ; address format: |
| 16 | + * ; [ 8 bit header | payload ]; |
| 17 | + * ; |
| 18 | + * ; shelley payment addresses: |
| 19 | + * ; bit 7: 0 |
| 20 | + * ; bit 6: base/other |
| 21 | + * ; bit 5: pointer/enterprise [for base: stake cred is keyhash/scripthash] |
| 22 | + * ; bit 4: payment cred is keyhash/scripthash |
| 23 | + * ; bits 3-0: network id |
| 24 | + * ; |
| 25 | + * ; reward addresses: |
| 26 | + * ; bits 7-5: 111 |
| 27 | + * ; bit 4: credential is keyhash/scripthash |
| 28 | + * ; bits 3-0: network id |
| 29 | + * ; |
| 30 | + * ; byron addresses: |
| 31 | + * ; bits 7-4: 1000 |
| 32 | + * ; |
| 33 | + * ; 0000: base address: keyhash28,keyhash28 |
| 34 | + * ; 0001: base address: scripthash28,keyhash28 |
| 35 | + * ; 0010: base address: keyhash28,scripthash28 |
| 36 | + * ; 0011: base address: scripthash28,scripthash28 |
| 37 | + * ; 0100: pointer address: keyhash28, 3 variable length uint |
| 38 | + * ; 0101: pointer address: scripthash28, 3 variable length uint |
| 39 | + * ; 0110: enterprise address: keyhash28 |
| 40 | + * ; 0111: enterprise address: scripthash28 |
| 41 | + * ; 1000: byron address |
| 42 | + * ; 1110: reward account: keyhash28 |
| 43 | + * ; 1111: reward account: scripthash28 |
| 44 | + * ; 1001-1101: future formats |
| 45 | + * ``` |
| 46 | + */ |
| 47 | + |
| 48 | +/** |
| 49 | + * Error thrown when address operations fail |
| 50 | + * |
| 51 | + * @since 2.0.0 |
| 52 | + * @category model |
| 53 | + */ |
| 54 | +export class AddressError extends Data.TaggedError("AddressError")<{ |
| 55 | + message?: string |
| 56 | + cause?: unknown |
| 57 | +}> {} |
| 58 | + |
| 59 | +/** |
| 60 | + * Union type representing all possible address types. |
| 61 | + * |
| 62 | + * @since 2.0.0 |
| 63 | + * @category model |
| 64 | + */ |
| 65 | +export const AddressEras = Schema.Union( |
| 66 | + BaseAddress.BaseAddress, |
| 67 | + EnterpriseAddress.EnterpriseAddress, |
| 68 | + PointerAddress.PointerAddress, |
| 69 | + RewardAccount.RewardAccount, |
| 70 | + ByronAddress.ByronAddress |
| 71 | +) |
| 72 | + |
| 73 | +export const isAddress = Schema.is(AddressEras) |
| 74 | + |
| 75 | +/** |
| 76 | + * Type representing an address. |
| 77 | + * |
| 78 | + * @since 2.0.0 |
| 79 | + * @category model |
| 80 | + */ |
| 81 | +export type AddressEras = typeof AddressEras.Type |
| 82 | + |
| 83 | +/** |
| 84 | + * Schema for encoding/decoding addresses as bytes. |
| 85 | + * |
| 86 | + * @since 2.0.0 |
| 87 | + * @category schema |
| 88 | + */ |
| 89 | +export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, AddressEras, { |
| 90 | + strict: true, |
| 91 | + encode: (_, __, ___, toA) => { |
| 92 | + switch (toA._tag) { |
| 93 | + case "BaseAddress": |
| 94 | + return ParseResult.encode(BaseAddress.FromBytes)(toA) |
| 95 | + case "EnterpriseAddress": |
| 96 | + return ParseResult.encode(EnterpriseAddress.FromBytes)(toA) |
| 97 | + case "PointerAddress": |
| 98 | + return ParseResult.encode(PointerAddress.FromBytes)(toA) |
| 99 | + case "RewardAccount": |
| 100 | + return ParseResult.encode(RewardAccount.FromBytes)(toA) |
| 101 | + case "ByronAddress": |
| 102 | + return ParseResult.encode(ByronAddress.BytesSchema)(toA) |
| 103 | + } |
| 104 | + }, |
| 105 | + decode: (_, __, ast, fromA) => |
| 106 | + Eff.gen(function* () { |
| 107 | + const header = fromA[0] |
| 108 | + // Extract address type from the upper 4 bits (bits 4-7) |
| 109 | + const addressType = header >> 4 |
| 110 | + |
| 111 | + switch (addressType) { |
| 112 | + // Base address types (0000, 0001, 0010, 0011) |
| 113 | + // Format: [payment credential, stake credential] |
| 114 | + case 0b0000: // Key payment, Key stake |
| 115 | + case 0b0001: // Script payment, Key stake |
| 116 | + case 0b0010: // Key payment, Script stake |
| 117 | + case 0b0011: |
| 118 | + return yield* ParseResult.decode(BaseAddress.FromBytes)(fromA) |
| 119 | + |
| 120 | + // Enterprise address types (0110, 0111) |
| 121 | + // Format: [payment credential only] |
| 122 | + case 0b0110: // Key payment |
| 123 | + case 0b0111: |
| 124 | + return yield* ParseResult.decode(EnterpriseAddress.FromBytes)(fromA) |
| 125 | + |
| 126 | + // Pointer address types (0100, 0101) |
| 127 | + // Format: [payment credential, variable length integers for slot, txIndex, certIndex] |
| 128 | + case 0b0100: // Key payment with pointer |
| 129 | + case 0b0101: |
| 130 | + return yield* ParseResult.decode(PointerAddress.FromBytes)(fromA) |
| 131 | + |
| 132 | + case 0b1110: |
| 133 | + case 0b1111: |
| 134 | + return yield* ParseResult.decode(RewardAccount.FromBytes)(fromA) |
| 135 | + |
| 136 | + case 0b1000: |
| 137 | + return yield* ParseResult.decode(ByronAddress.BytesSchema)(fromA) |
| 138 | + |
| 139 | + default: |
| 140 | + return yield* ParseResult.fail(new ParseResult.Type(ast, fromA, `Unknown address type: ${addressType}`)) |
| 141 | + } |
| 142 | + }) |
| 143 | +}) |
| 144 | + |
| 145 | +/** |
| 146 | + * Schema for encoding/decoding addresses as hex strings. |
| 147 | + * |
| 148 | + * @since 2.0.0 |
| 149 | + * @category schema |
| 150 | + */ |
| 151 | +export const FromHex = Schema.compose(Bytes.FromHex, FromBytes) |
| 152 | + |
| 153 | +/** |
| 154 | + * Schema for encoding/decoding addresses as Bech32 strings. |
| 155 | + * |
| 156 | + * @since 2.0.0 |
| 157 | + * @category schema |
| 158 | + */ |
| 159 | +export const FromBech32 = Schema.transformOrFail(Schema.String, AddressEras, { |
| 160 | + strict: true, |
| 161 | + encode: (_, __, ast, toA) => |
| 162 | + Eff.gen(function* () { |
| 163 | + const bytes = yield* ParseResult.encode(FromBytes)(toA) |
| 164 | + let prefix: string |
| 165 | + switch (toA._tag) { |
| 166 | + case "BaseAddress": |
| 167 | + case "EnterpriseAddress": |
| 168 | + case "PointerAddress": |
| 169 | + prefix = toA.networkId === 0 ? "addr_test" : "addr" |
| 170 | + break |
| 171 | + case "RewardAccount": |
| 172 | + prefix = toA.networkId === 0 ? "stake_test" : "stake" |
| 173 | + break |
| 174 | + case "ByronAddress": |
| 175 | + return yield* ParseResult.fail( |
| 176 | + new ParseResult.Type(ast, toA, "Byron addresses do not support Bech32 encoding") |
| 177 | + ) |
| 178 | + } |
| 179 | + const result = yield* Eff.try({ |
| 180 | + try: () => { |
| 181 | + const words = bech32.toWords(bytes) |
| 182 | + return bech32.encode(prefix, words, false) |
| 183 | + }, |
| 184 | + catch: (error) => new ParseResult.Type(ast, toA, `Failed to encode Bech32: ${(error as Error).message}`) |
| 185 | + }) |
| 186 | + return result |
| 187 | + }), |
| 188 | + decode: (fromA, _, ast) => |
| 189 | + Eff.gen(function* () { |
| 190 | + const result = yield* Eff.try({ |
| 191 | + try: () => { |
| 192 | + const decoded = bech32.decode(fromA as any, false) |
| 193 | + const bytes = bech32.fromWords(decoded.words) |
| 194 | + return new Uint8Array(bytes) |
| 195 | + }, |
| 196 | + catch: (error) => new ParseResult.Type(ast, fromA, `Failed to decode Bech32: ${(error as Error).message}`) |
| 197 | + }) |
| 198 | + return yield* ParseResult.decode(FromBytes)(result) |
| 199 | + }) |
| 200 | +}).annotations({ |
| 201 | + identifier: "Address.FromBech32", |
| 202 | + description: "Transforms Bech32 string to Address" |
| 203 | +}) |
| 204 | + |
| 205 | +/** |
| 206 | + * Checks if two addresses are equal. |
| 207 | + * |
| 208 | + * @since 2.0.0 |
| 209 | + * @category utils |
| 210 | + */ |
| 211 | +export const equals = (a: AddressEras, b: AddressEras): boolean => { |
| 212 | + if (a._tag !== b._tag) { |
| 213 | + return false |
| 214 | + } |
| 215 | + switch (a._tag) { |
| 216 | + case "BaseAddress": |
| 217 | + return BaseAddress.equals(a, b as BaseAddress.BaseAddress) |
| 218 | + case "EnterpriseAddress": |
| 219 | + return EnterpriseAddress.equals(a, b as EnterpriseAddress.EnterpriseAddress) |
| 220 | + case "PointerAddress": |
| 221 | + return PointerAddress.equals(a, b as PointerAddress.PointerAddress) |
| 222 | + case "RewardAccount": |
| 223 | + return RewardAccount.equals(a, b as RewardAccount.RewardAccount) |
| 224 | + case "ByronAddress": |
| 225 | + return false |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +/** |
| 230 | + * FastCheck arbitrary for Address instances. |
| 231 | + * |
| 232 | + * @since 2.0.0 |
| 233 | + * @category arbitrary |
| 234 | + * |
| 235 | + */ |
| 236 | +export const arbitrary = FastCheck.oneof( |
| 237 | + BaseAddress.arbitrary, |
| 238 | + EnterpriseAddress.arbitrary, |
| 239 | + PointerAddress.arbitrary, |
| 240 | + RewardAccount.arbitrary |
| 241 | +) |
| 242 | + |
| 243 | +// ============================================================================ |
| 244 | +// Parsing Functions |
| 245 | +// ============================================================================ |
| 246 | + |
| 247 | +/** |
| 248 | + * Parse an Address from bytes. |
| 249 | + * |
| 250 | + * @since 2.0.0 |
| 251 | + * @category parsing |
| 252 | + */ |
| 253 | +export const fromBytes = Function.makeDecodeSync(FromBytes, AddressError, "Address.fromBytes") |
| 254 | + |
| 255 | +/** |
| 256 | + * Parse an Address from hex string. |
| 257 | + * |
| 258 | + * @since 2.0.0 |
| 259 | + * @category parsing |
| 260 | + */ |
| 261 | +export const fromHex = Function.makeDecodeSync(FromHex, AddressError, "Address.fromHex") |
| 262 | + |
| 263 | +/** |
| 264 | + * Parse an Address from Bech32 string. |
| 265 | + * |
| 266 | + * @since 2.0.0 |
| 267 | + * @category parsing |
| 268 | + */ |
| 269 | +export const fromBech32 = Function.makeDecodeSync(FromBech32, AddressError, "Address.fromBech32") |
| 270 | + |
| 271 | +// ============================================================================ |
| 272 | +// Encoding Functions |
| 273 | +// ============================================================================ |
| 274 | + |
| 275 | +/** |
| 276 | + * Convert an Address to bytes. |
| 277 | + * |
| 278 | + * @since 2.0.0 |
| 279 | + * @category encoding |
| 280 | + */ |
| 281 | +export const toBytes = Function.makeEncodeSync(FromBytes, AddressError, "Address.toBytes") |
| 282 | + |
| 283 | +/** |
| 284 | + * Convert an Address to hex string. |
| 285 | + * |
| 286 | + * @since 2.0.0 |
| 287 | + * @category encoding |
| 288 | + */ |
| 289 | +export const toHex = Function.makeEncodeSync(FromHex, AddressError, "Address.toHex") |
| 290 | + |
| 291 | +/** |
| 292 | + * Convert an Address to Bech32 string. |
| 293 | + * |
| 294 | + * @since 2.0.0 |
| 295 | + * @category encoding |
| 296 | + */ |
| 297 | +export const toBech32 = Function.makeEncodeSync(FromBech32, AddressError, "Address.toBech32") |
| 298 | + |
| 299 | +/** |
| 300 | + * Effect-based error handling variants for functions that can fail. |
| 301 | + * |
| 302 | + * @since 2.0.0 |
| 303 | + * @category effect |
| 304 | + */ |
| 305 | +export namespace Either { |
| 306 | + export const fromBytes = Function.makeDecodeEither(FromBytes, AddressError) |
| 307 | + export const fromHex = Function.makeDecodeEither(FromHex, AddressError) |
| 308 | + export const fromBech32 = Function.makeDecodeEither(FromBech32, AddressError) |
| 309 | + |
| 310 | + export const toBytes = Function.makeEncodeEither(FromBytes, AddressError) |
| 311 | + export const toHex = Function.makeEncodeEither(FromHex, AddressError) |
| 312 | + export const toBech32 = Function.makeEncodeEither(FromBech32, AddressError) |
| 313 | +} |
0 commit comments