Skip to content

Commit c682347

Browse files
committed
feat(txbuilder): add builders
1 parent 6533312 commit c682347

File tree

13 files changed

+3559
-0
lines changed

13 files changed

+3559
-0
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import { Data, Effect as Eff } from "effect"
2+
3+
import type * as Certificate from "../core/Certificate.js"
4+
import type * as Credential from "../core/Credential.js"
5+
import * as KeyHash from "../core/KeyHash.js"
6+
import type * as NativeScripts from "../core/NativeScripts.js"
7+
import type * as PoolKeyHash from "../core/PoolKeyHash.js"
8+
import * as ScriptHash from "../core/ScriptHash.js"
9+
import type { NativeScriptWitnessInfo, PartialPlutusWitness } from "./WitnessBuilder.js"
10+
import { InputAggregateWitnessData, PlutusScriptWitness, RequiredWitnessSet } from "./WitnessBuilder.js"
11+
12+
/**
13+
* Error class for CertificateBuilder related operations.
14+
*
15+
* @since 2.0.0
16+
* @category errors
17+
*/
18+
export class CertificateBuilderError extends Data.TaggedError("CertificateBuilderError")<{
19+
message?: string
20+
cause?: unknown
21+
}> {}
22+
23+
/**
24+
* Calculates required witnesses for a certificate
25+
*
26+
* @since 2.0.0
27+
* @category utils
28+
*/
29+
export function certRequiredWits(cert: Certificate.Certificate, requiredWitnesses: RequiredWitnessSet): void {
30+
switch (cert._tag) {
31+
case "StakeRegistration":
32+
// Stake key registrations do not require a witness
33+
break
34+
35+
case "StakeDeregistration":
36+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
37+
break
38+
39+
case "StakeDelegation":
40+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
41+
break
42+
43+
case "PoolRegistration":
44+
cert.poolParams.poolOwners.forEach((owner) => {
45+
requiredWitnesses.addVkeyKeyHash(owner) // owner is already KeyHash
46+
})
47+
requiredWitnesses.addVkeyKeyHash(poolKeyHashToKeyHash(cert.poolParams.operator)) // operator is PoolKeyHash
48+
break
49+
50+
case "PoolRetirement":
51+
requiredWitnesses.addVkeyKeyHash(poolKeyHashToKeyHash(cert.poolKeyHash))
52+
break
53+
54+
case "RegCert":
55+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
56+
break
57+
58+
case "UnregCert":
59+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
60+
break
61+
62+
case "VoteDelegCert":
63+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
64+
break
65+
66+
case "StakeVoteDelegCert":
67+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
68+
break
69+
70+
case "StakeRegDelegCert":
71+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
72+
break
73+
74+
case "VoteRegDelegCert":
75+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
76+
break
77+
78+
case "StakeVoteRegDelegCert":
79+
addCredentialWitness(cert.stakeCredential, requiredWitnesses)
80+
break
81+
82+
case "AuthCommitteeHotCert":
83+
addCredentialWitness(cert.committeeColdCredential, requiredWitnesses)
84+
break
85+
86+
case "ResignCommitteeColdCert":
87+
addCredentialWitness(cert.committeeColdCredential, requiredWitnesses)
88+
break
89+
90+
case "RegDrepCert":
91+
addCredentialWitness(cert.drepCredential, requiredWitnesses)
92+
break
93+
94+
case "UnregDrepCert":
95+
addCredentialWitness(cert.drepCredential, requiredWitnesses)
96+
break
97+
98+
case "UpdateDrepCert":
99+
addCredentialWitness(cert.drepCredential, requiredWitnesses)
100+
break
101+
}
102+
}
103+
104+
function addCredentialWitness(credential: Credential.CredentialSchema, requiredWitnesses: RequiredWitnessSet): void {
105+
switch (credential._tag) {
106+
case "KeyHash":
107+
requiredWitnesses.addVkeyKeyHash(credential)
108+
break
109+
case "ScriptHash":
110+
requiredWitnesses.addScriptHash(credential)
111+
break
112+
}
113+
}
114+
115+
function poolKeyHashToKeyHash(poolKeyHash: PoolKeyHash.PoolKeyHash): KeyHash.KeyHash {
116+
// Both PoolKeyHash and KeyHash are based on Hash28, so we can convert by extracting the hash
117+
return KeyHash.make({ hash: poolKeyHash.hash })
118+
}
119+
120+
/**
121+
* Result of building a certificate
122+
*
123+
* @since 2.0.0
124+
* @category model
125+
*/
126+
export interface CertificateBuilderResult {
127+
cert: Certificate.Certificate
128+
aggregateWitness?: InputAggregateWitnessData
129+
requiredWits: RequiredWitnessSet
130+
}
131+
132+
/**
133+
* Builder for a single certificate
134+
*
135+
* @since 2.0.0
136+
* @category builders
137+
*/
138+
export class SingleCertificateBuilder {
139+
constructor(public readonly cert: Certificate.Certificate) {}
140+
141+
static new(cert: Certificate.Certificate): SingleCertificateBuilder {
142+
return new SingleCertificateBuilder(cert)
143+
}
144+
145+
skipWitness(): CertificateBuilderResult {
146+
const requiredWits = RequiredWitnessSet.default()
147+
certRequiredWits(this.cert, requiredWits)
148+
149+
return {
150+
cert: this.cert,
151+
aggregateWitness: undefined,
152+
requiredWits
153+
}
154+
}
155+
156+
paymentKey(): Eff.Effect<CertificateBuilderResult, CertificateBuilderError> {
157+
return Eff.gen(
158+
function* (this: SingleCertificateBuilder) {
159+
const requiredWits = RequiredWitnessSet.default()
160+
certRequiredWits(this.cert, requiredWits)
161+
162+
if (requiredWits.scripts.length > 0) {
163+
return yield* Eff.fail(
164+
new CertificateBuilderError({
165+
message: `Certificate contains script. Expected public key hash.`
166+
})
167+
)
168+
}
169+
170+
return {
171+
cert: this.cert,
172+
aggregateWitness: undefined,
173+
requiredWits
174+
}
175+
}.bind(this)
176+
)
177+
}
178+
179+
nativeScript(
180+
nativeScript: NativeScripts.NativeScript,
181+
witnessInfo: NativeScriptWitnessInfo
182+
): Eff.Effect<CertificateBuilderResult, CertificateBuilderError> {
183+
return Eff.gen(
184+
function* (this: SingleCertificateBuilder) {
185+
const requiredWits = RequiredWitnessSet.default()
186+
certRequiredWits(this.cert, requiredWits)
187+
const requiredWitsLeft = structuredClone(requiredWits)
188+
189+
const scriptHash = ScriptHash.fromScript(nativeScript)
190+
191+
// Check if the script is actually required
192+
const contains = requiredWitsLeft.scripts.some((h) => ScriptHash.equals(h, scriptHash))
193+
194+
// Remove the script hash
195+
const filteredScripts = requiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash))
196+
const mutableRequiredWitsLeft = { ...requiredWitsLeft, scripts: filteredScripts }
197+
198+
if (mutableRequiredWitsLeft.scripts.length > 0) {
199+
return yield* Eff.fail(
200+
new CertificateBuilderError({
201+
message: "Missing the following witnesses for the certificate",
202+
cause: mutableRequiredWitsLeft
203+
})
204+
)
205+
}
206+
207+
return {
208+
cert: this.cert,
209+
aggregateWitness: contains ? InputAggregateWitnessData.nativeScript(nativeScript, witnessInfo) : undefined,
210+
requiredWits
211+
}
212+
}.bind(this)
213+
)
214+
}
215+
216+
plutusScript(
217+
partialWitness: PartialPlutusWitness,
218+
requiredSigners: Array<KeyHash.KeyHash>
219+
): Eff.Effect<CertificateBuilderResult, CertificateBuilderError> {
220+
return Eff.gen(
221+
function* (this: SingleCertificateBuilder) {
222+
const requiredWits = RequiredWitnessSet.default()
223+
requiredSigners.forEach((signer) => requiredWits.addVkeyKeyHash(signer))
224+
certRequiredWits(this.cert, requiredWits)
225+
const requiredWitsLeft = structuredClone(requiredWits)
226+
227+
// Clear vkeys as we don't know which ones will be used
228+
const mutableRequiredWitsLeft = { ...requiredWitsLeft, vkeys: [] }
229+
230+
const scriptHash = PlutusScriptWitness.hash(partialWitness.scriptWitness)
231+
232+
// Check if the script is actually required
233+
const contains = requiredWitsLeft.scripts.some((h) => ScriptHash.equals(h, scriptHash))
234+
235+
// Remove the script hash
236+
const filteredPlutusScripts = mutableRequiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash))
237+
const finalRequiredWitsLeft = new RequiredWitnessSet({
238+
vkeys: mutableRequiredWitsLeft.vkeys,
239+
bootstraps: mutableRequiredWitsLeft.bootstraps,
240+
scripts: filteredPlutusScripts,
241+
plutusData: mutableRequiredWitsLeft.plutusData,
242+
redeemers: mutableRequiredWitsLeft.redeemers,
243+
scriptRefs: mutableRequiredWitsLeft.scriptRefs
244+
})
245+
246+
if (finalRequiredWitsLeft.len() > 0) {
247+
return yield* Eff.fail(
248+
new CertificateBuilderError({
249+
message: "Missing the following witnesses for the certificate",
250+
cause: finalRequiredWitsLeft
251+
})
252+
)
253+
}
254+
255+
return {
256+
cert: this.cert,
257+
aggregateWitness: contains
258+
? InputAggregateWitnessData.plutusScript(
259+
partialWitness,
260+
requiredSigners,
261+
undefined // No datum for certificates
262+
)
263+
: undefined,
264+
requiredWits
265+
}
266+
}.bind(this)
267+
)
268+
}
269+
}

0 commit comments

Comments
 (0)