Skip to content

Commit b069c43

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

File tree

19 files changed

+3551
-36
lines changed

19 files changed

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

0 commit comments

Comments
 (0)