Skip to content

Commit 0d75af0

Browse files
Merge pull request #96 from IntersectMBO/feat/governance
feat: add CIP-129 bech32 for governance
2 parents ed23f8a + aa9a073 commit 0d75af0

File tree

3 files changed

+523
-29
lines changed

3 files changed

+523
-29
lines changed

packages/evolution/src/core/CommitteeColdCredential.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,192 @@
44
* In Cardano, committee_cold_credential = credential, representing the same credential structure
55
* but used specifically for committee cold keys in governance.
66
*
7+
* Implements CIP-129 bech32 encoding with "cc_cold" prefix.
8+
*
79
* @since 2.0.0
810
*/
911

12+
import { bech32 } from "@scure/base"
13+
import * as Effect from "effect/Effect"
14+
import * as ParseResult from "effect/ParseResult"
15+
import * as Schema from "effect/Schema"
16+
1017
import * as Credential from "./Credential.js"
18+
import * as KeyHash from "./KeyHash.js"
19+
import * as ScriptHash from "./ScriptHash.js"
1120

1221
export const CommitteeColdCredential = Credential
22+
23+
// ============================================================================
24+
// CIP-129 Bech32 Support
25+
// ============================================================================
26+
27+
/**
28+
* Transform from CIP-129 bytes (29 bytes) to Committee Cold Credential.
29+
* Format: [header_byte(1)][credential_bytes(28)]
30+
* Header byte for cc_cold:
31+
* - 0x1C = KeyHash (bits: 0001 1100 = key type 0x01, cred type 0x0C)
32+
* - 0x1D = ScriptHash (bits: 0001 1101 = key type 0x01, cred type 0x0D)
33+
*
34+
* @since 2.0.0
35+
* @category transformations
36+
*/
37+
export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, Schema.typeSchema(Credential.CredentialSchema), {
38+
strict: true,
39+
encode: (toI, _, ast) =>
40+
Effect.gen(function* () {
41+
// Encode: Credential → 29 bytes with cc_cold header
42+
const credBytes = toI.hash
43+
44+
if (credBytes.length !== 28) {
45+
return yield* ParseResult.fail(
46+
new ParseResult.Type(ast, toI, `Invalid credential hash length: expected 28 bytes, got ${credBytes.length}`)
47+
)
48+
}
49+
50+
const header = toI._tag === "KeyHash" ? 0x1c : 0x1d
51+
const result = new Uint8Array(29)
52+
result[0] = header
53+
result.set(credBytes, 1)
54+
return result
55+
}),
56+
decode: (fromA, _, ast) =>
57+
Effect.gen(function* () {
58+
// Decode: 29 bytes → Credential
59+
if (fromA.length !== 29) {
60+
return yield* ParseResult.fail(
61+
new ParseResult.Type(ast, fromA, `Invalid cc_cold credential length: expected 29 bytes, got ${fromA.length}`)
62+
)
63+
}
64+
65+
const header = fromA[0]
66+
const credBytes = fromA.slice(1)
67+
68+
// Validate header byte
69+
const keyType = (header >> 4) & 0x0f
70+
const credType = header & 0x0f
71+
72+
if (keyType !== 0x01) {
73+
return yield* ParseResult.fail(
74+
new ParseResult.Type(ast, fromA, `Invalid key type in header: expected 0x01 (CC Cold), got 0x0${keyType.toString(16)}`)
75+
)
76+
}
77+
78+
if (credType === 0x0c) {
79+
const keyHash = yield* ParseResult.decode(KeyHash.FromBytes)(credBytes)
80+
return new KeyHash.KeyHash({ hash: keyHash.hash })
81+
} else if (credType === 0x0d) {
82+
const scriptHash = yield* ParseResult.decode(ScriptHash.FromBytes)(credBytes)
83+
return new ScriptHash.ScriptHash({ hash: scriptHash.hash })
84+
}
85+
86+
return yield* ParseResult.fail(
87+
new ParseResult.Type(ast, fromA, `Invalid credential type in header: expected 0x0C or 0x0D, got 0x0${credType.toString(16)}`)
88+
)
89+
})
90+
}).annotations({
91+
identifier: "CommitteeColdCredential.FromBytes",
92+
description: "Transforms CIP-129 bytes to Committee Cold Credential"
93+
})
94+
95+
/**
96+
* Transform from hex string to Committee Cold Credential.
97+
*
98+
* @since 2.0.0
99+
* @category transformations
100+
*/
101+
export const FromHex = Schema.compose(Schema.Uint8ArrayFromHex, FromBytes).annotations({
102+
identifier: "CommitteeColdCredential.FromHex",
103+
description: "Transforms hex string to Committee Cold Credential"
104+
})
105+
106+
/**
107+
* Transform from Bech32 string to Committee Cold Credential following CIP-129.
108+
* Bech32 prefix: "cc_cold" for both KeyHash and ScriptHash
109+
*
110+
* @since 2.0.0
111+
* @category transformations
112+
*/
113+
export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchema(Credential.CredentialSchema), {
114+
strict: true,
115+
encode: (_, __, ___, toA) =>
116+
Effect.gen(function* () {
117+
const bytes = yield* ParseResult.encode(FromBytes)(toA)
118+
const words = bech32.toWords(bytes)
119+
return bech32.encode("cc_cold", words, false)
120+
}),
121+
decode: (fromA, _, ast) =>
122+
Effect.gen(function* () {
123+
const result = yield* Effect.try({
124+
try: () => {
125+
const decoded = bech32.decode(fromA as any, false)
126+
if (decoded.prefix !== "cc_cold") {
127+
throw new Error(`Invalid prefix: expected "cc_cold", got "${decoded.prefix}"`)
128+
}
129+
const bytes = bech32.fromWords(decoded.words)
130+
return new Uint8Array(bytes)
131+
},
132+
catch: (error) => new ParseResult.Type(ast, fromA, `Failed to decode bech32: ${error}`)
133+
})
134+
return yield* ParseResult.decode(FromBytes)(result)
135+
})
136+
}).annotations({
137+
identifier: "CommitteeColdCredential.FromBech32",
138+
description: "Transforms Bech32 string to Committee Cold Credential (CIP-129)"
139+
})
140+
141+
// ============================================================================
142+
// Decoding Functions
143+
// ============================================================================
144+
145+
/**
146+
* Parse Committee Cold Credential from CIP-129 bytes.
147+
*
148+
* @since 2.0.0
149+
* @category parsing
150+
*/
151+
export const fromBytes = Schema.decodeSync(FromBytes)
152+
153+
/**
154+
* Parse Committee Cold Credential from hex string.
155+
*
156+
* @since 2.0.0
157+
* @category parsing
158+
*/
159+
export const fromHex = Schema.decodeSync(FromHex)
160+
161+
/**
162+
* Parse Committee Cold Credential from Bech32 string (CIP-129 format).
163+
*
164+
* @since 2.0.0
165+
* @category parsing
166+
*/
167+
export const fromBech32 = Schema.decodeSync(FromBech32)
168+
169+
// ============================================================================
170+
// Encoding Functions
171+
// ============================================================================
172+
173+
/**
174+
* Encode Committee Cold Credential to CIP-129 bytes.
175+
*
176+
* @since 2.0.0
177+
* @category encoding
178+
*/
179+
export const toBytes = Schema.encodeSync(FromBytes)
180+
181+
/**
182+
* Encode Committee Cold Credential to hex string.
183+
*
184+
* @since 2.0.0
185+
* @category encoding
186+
*/
187+
export const toHex = Schema.encodeSync(FromHex)
188+
189+
/**
190+
* Encode Committee Cold Credential to Bech32 string (CIP-129 format).
191+
*
192+
* @since 2.0.0
193+
* @category encoding
194+
*/
195+
export const toBech32 = Schema.encodeSync(FromBech32)

packages/evolution/src/core/CommitteeHotCredential.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,192 @@
44
* In Cardano, committee_hot_credential = credential, representing the same credential structure
55
* but used specifically for committee hot keys in governance.
66
*
7+
* Implements CIP-129 bech32 encoding with "cc_hot" prefix.
8+
*
79
* @since 2.0.0
810
*/
911

12+
import { bech32 } from "@scure/base"
13+
import * as Effect from "effect/Effect"
14+
import * as ParseResult from "effect/ParseResult"
15+
import * as Schema from "effect/Schema"
16+
1017
import * as Credential from "./Credential.js"
18+
import * as KeyHash from "./KeyHash.js"
19+
import * as ScriptHash from "./ScriptHash.js"
1120

1221
export const CommitteeHotCredential = Credential
22+
23+
// ============================================================================
24+
// CIP-129 Bech32 Support
25+
// ============================================================================
26+
27+
/**
28+
* Transform from CIP-129 bytes (29 bytes) to Committee Hot Credential.
29+
* Format: [header_byte(1)][credential_bytes(28)]
30+
* Header byte for cc_hot:
31+
* - 0x1E = KeyHash (bits: 0001 1110 = key type 0x01, cred type 0x0E)
32+
* - 0x1F = ScriptHash (bits: 0001 1111 = key type 0x01, cred type 0x0F)
33+
*
34+
* @since 2.0.0
35+
* @category transformations
36+
*/
37+
export const FromBytes = Schema.transformOrFail(Schema.Uint8ArrayFromSelf, Schema.typeSchema(Credential.CredentialSchema), {
38+
strict: true,
39+
encode: (toI, _, ast) =>
40+
Effect.gen(function* () {
41+
// Encode: Credential → 29 bytes with cc_hot header
42+
const credBytes = toI.hash
43+
44+
if (credBytes.length !== 28) {
45+
return yield* ParseResult.fail(
46+
new ParseResult.Type(ast, toI, `Invalid credential hash length: expected 28 bytes, got ${credBytes.length}`)
47+
)
48+
}
49+
50+
const header = toI._tag === "KeyHash" ? 0x1e : 0x1f
51+
const result = new Uint8Array(29)
52+
result[0] = header
53+
result.set(credBytes, 1)
54+
return result
55+
}),
56+
decode: (fromA, _, ast) =>
57+
Effect.gen(function* () {
58+
// Decode: 29 bytes → Credential
59+
if (fromA.length !== 29) {
60+
return yield* ParseResult.fail(
61+
new ParseResult.Type(ast, fromA, `Invalid cc_hot credential length: expected 29 bytes, got ${fromA.length}`)
62+
)
63+
}
64+
65+
const header = fromA[0]
66+
const credBytes = fromA.slice(1)
67+
68+
// Validate header byte
69+
const keyType = (header >> 4) & 0x0f
70+
const credType = header & 0x0f
71+
72+
if (keyType !== 0x01) {
73+
return yield* ParseResult.fail(
74+
new ParseResult.Type(ast, fromA, `Invalid key type in header: expected 0x01 (CC Hot), got 0x0${keyType.toString(16)}`)
75+
)
76+
}
77+
78+
if (credType === 0x0e) {
79+
const keyHash = yield* ParseResult.decode(KeyHash.FromBytes)(credBytes)
80+
return new KeyHash.KeyHash({ hash: keyHash.hash })
81+
} else if (credType === 0x0f) {
82+
const scriptHash = yield* ParseResult.decode(ScriptHash.FromBytes)(credBytes)
83+
return new ScriptHash.ScriptHash({ hash: scriptHash.hash })
84+
}
85+
86+
return yield* ParseResult.fail(
87+
new ParseResult.Type(ast, fromA, `Invalid credential type in header: expected 0x0E or 0x0F, got 0x0${credType.toString(16)}`)
88+
)
89+
})
90+
}).annotations({
91+
identifier: "CommitteeHotCredential.FromBytes",
92+
description: "Transforms CIP-129 bytes to Committee Hot Credential"
93+
})
94+
95+
/**
96+
* Transform from hex string to Committee Hot Credential.
97+
*
98+
* @since 2.0.0
99+
* @category transformations
100+
*/
101+
export const FromHex = Schema.compose(Schema.Uint8ArrayFromHex, FromBytes).annotations({
102+
identifier: "CommitteeHotCredential.FromHex",
103+
description: "Transforms hex string to Committee Hot Credential"
104+
})
105+
106+
/**
107+
* Transform from Bech32 string to Committee Hot Credential following CIP-129.
108+
* Bech32 prefix: "cc_hot" for both KeyHash and ScriptHash
109+
*
110+
* @since 2.0.0
111+
* @category transformations
112+
*/
113+
export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchema(Credential.CredentialSchema), {
114+
strict: true,
115+
encode: (_, __, ___, toA) =>
116+
Effect.gen(function* () {
117+
const bytes = yield* ParseResult.encode(FromBytes)(toA)
118+
const words = bech32.toWords(bytes)
119+
return bech32.encode("cc_hot", words, false)
120+
}),
121+
decode: (fromA, _, ast) =>
122+
Effect.gen(function* () {
123+
const result = yield* Effect.try({
124+
try: () => {
125+
const decoded = bech32.decode(fromA as any, false)
126+
if (decoded.prefix !== "cc_hot") {
127+
throw new Error(`Invalid prefix: expected "cc_hot", got "${decoded.prefix}"`)
128+
}
129+
const bytes = bech32.fromWords(decoded.words)
130+
return new Uint8Array(bytes)
131+
},
132+
catch: (error) => new ParseResult.Type(ast, fromA, `Failed to decode bech32: ${error}`)
133+
})
134+
return yield* ParseResult.decode(FromBytes)(result)
135+
})
136+
}).annotations({
137+
identifier: "CommitteeHotCredential.FromBech32",
138+
description: "Transforms Bech32 string to Committee Hot Credential (CIP-129)"
139+
})
140+
141+
// ============================================================================
142+
// Decoding Functions
143+
// ============================================================================
144+
145+
/**
146+
* Parse Committee Hot Credential from CIP-129 bytes.
147+
*
148+
* @since 2.0.0
149+
* @category parsing
150+
*/
151+
export const fromBytes = Schema.decodeSync(FromBytes)
152+
153+
/**
154+
* Parse Committee Hot Credential from hex string.
155+
*
156+
* @since 2.0.0
157+
* @category parsing
158+
*/
159+
export const fromHex = Schema.decodeSync(FromHex)
160+
161+
/**
162+
* Parse Committee Hot Credential from Bech32 string (CIP-129 format).
163+
*
164+
* @since 2.0.0
165+
* @category parsing
166+
*/
167+
export const fromBech32 = Schema.decodeSync(FromBech32)
168+
169+
// ============================================================================
170+
// Encoding Functions
171+
// ============================================================================
172+
173+
/**
174+
* Encode Committee Hot Credential to CIP-129 bytes.
175+
*
176+
* @since 2.0.0
177+
* @category encoding
178+
*/
179+
export const toBytes = Schema.encodeSync(FromBytes)
180+
181+
/**
182+
* Encode Committee Hot Credential to hex string.
183+
*
184+
* @since 2.0.0
185+
* @category encoding
186+
*/
187+
export const toHex = Schema.encodeSync(FromHex)
188+
189+
/**
190+
* Encode Committee Hot Credential to Bech32 string (CIP-129 format).
191+
*
192+
* @since 2.0.0
193+
* @category encoding
194+
*/
195+
export const toBech32 = Schema.encodeSync(FromBech32)

0 commit comments

Comments
 (0)