Skip to content

Commit 6f85cfd

Browse files
committed
feat: transaction output with tests
1 parent d58a3da commit 6f85cfd

8 files changed

Lines changed: 952 additions & 75 deletions

packages/experimental/src/BabbageTransactionOutput.ts

Lines changed: 160 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// packages/experimental/src/BabbageTransactionOutput.ts
2+
13
import {
24
Data,
35
Effect,
@@ -11,6 +13,7 @@ import {
1113
import * as CBOR from "./CBOR.js";
1214
import * as Hex from "./Hex.js";
1315
import * as Address from "./Address.js";
16+
import * as DatumOption from "./DatumOption.js";
1417

1518
/**
1619
* CDDL specs
@@ -30,7 +33,7 @@ export class BabbageTransactionOutput extends Schema.TaggedClass<BabbageTransact
3033
)("BabbageTransactionOutput", {
3134
address: Address.Address,
3235
value: Schema.BigIntFromSelf,
33-
datumOption: Schema.optional(Schema.Uint8ArrayFromSelf),
36+
datumOption: Schema.optional(DatumOption.DatumOption),
3437
scriptRef: Schema.optional(Schema.Uint8ArrayFromSelf),
3538
}) {
3639
[Inspectable.NodeInspectSymbol]() {
@@ -66,7 +69,7 @@ export class BabbageTransactionOutputError extends Data.TaggedError(
6669
export const isBabbageTransactionOutput = Schema.is(BabbageTransactionOutput);
6770

6871
/**
69-
* Schema for transforming between CBOR bytes and BabbageTransactionOutput
72+
* Schema for transforming between CBOR bytes and BabbageTransactionOutput.
7073
*
7174
* @since 2.0.0
7275
* @category encoding/decoding
@@ -80,59 +83,144 @@ export const CBORBytes = Schema.transformOrFail(
8083
strict: true,
8184
encode: (toI, options, ast, toA) =>
8285
pipe(
83-
ParseResult.encode(Address.Address)(toA.address),
84-
Effect.map((addressBytes) => {
86+
ParseResult.encode(Address.BytesSchema)(toA.address),
87+
Effect.flatMap((addressBytes) => {
8588
const map = new Map<number, unknown>([
8689
[0, addressBytes],
8790
[1, toA.value],
8891
]);
8992

90-
if (toA.datumOption) {
91-
map.set(2, toA.datumOption);
93+
// Encode datumOption if present
94+
const datumOptionEffect =
95+
toA.datumOption !== undefined
96+
? pipe(
97+
ParseResult.encode(DatumOption.CBORBytes)(toA.datumOption),
98+
Effect.map((encodedDatum) => {
99+
map.set(2, encodedDatum);
100+
return map;
101+
}),
102+
)
103+
: Effect.succeed(map);
104+
105+
return pipe(
106+
datumOptionEffect,
107+
Effect.map((updatedMap) => {
108+
if (toA.scriptRef !== undefined) {
109+
updatedMap.set(3, toA.scriptRef);
110+
}
111+
// Ensure we encode a Map, as per CDDL
112+
return CBOR.encodeAsBytesOrThrow(updatedMap);
113+
}),
114+
);
115+
}),
116+
),
117+
decode: (fromA, options, ast, fromI) =>
118+
pipe(
119+
CBOR.decodeBytes(fromA),
120+
Effect.mapError(
121+
(e) =>
122+
new ParseResult.Type(
123+
ast,
124+
fromA,
125+
`CBOR decoding failed: ${e.message}`,
126+
),
127+
),
128+
Effect.flatMap((decodedData) => {
129+
const getField = (key: number): unknown => {
130+
if (decodedData instanceof Map) {
131+
return decodedData.get(key);
132+
} else if (
133+
typeof decodedData === "object" &&
134+
decodedData !== null
135+
) {
136+
return (decodedData as Record<number, unknown>)[key];
137+
}
138+
return undefined; // If decodedData is neither expected type, return undefined
139+
};
140+
141+
const addressBytes = getField(0);
142+
const valueRaw = getField(1);
143+
const datumOptionBytes = getField(2);
144+
const scriptRefRaw = getField(3);
145+
146+
if (addressBytes === undefined || valueRaw === undefined) {
147+
return ParseResult.fail(
148+
new ParseResult.Type(
149+
ast,
150+
fromA,
151+
"Missing required fields (address (0) or value (1)) in decoded CBOR data for BabbageTransactionOutput.",
152+
),
153+
);
92154
}
93155

94-
if (toA.scriptRef) {
95-
map.set(3, toA.scriptRef);
156+
if (
157+
!(decodedData instanceof Map) &&
158+
!(typeof decodedData === "object" && decodedData !== null)
159+
) {
160+
return ParseResult.fail(
161+
new ParseResult.Type(
162+
ast,
163+
fromA,
164+
`Expected a Map or a plain object for BabbageTransactionOutput, but got ${typeof decodedData}.`,
165+
),
166+
);
96167
}
97168

98-
return CBOR.encodeAsBytesOrThrow(map);
169+
return Effect.gen(function* () {
170+
// Decode and validate each field using its respective schema
171+
const address = yield* ParseResult.decodeUnknown(
172+
Address.BytesSchema,
173+
)(addressBytes);
174+
175+
const value = yield* ParseResult.decodeUnknown(
176+
Schema.BigIntFromSelf,
177+
)(valueRaw);
178+
179+
const datumOption =
180+
datumOptionBytes !== undefined
181+
? yield* ParseResult.decodeUnknown(DatumOption.CBORBytes)(
182+
datumOptionBytes,
183+
)
184+
: undefined;
185+
186+
const scriptRef =
187+
scriptRefRaw !== undefined
188+
? yield* ParseResult.decodeUnknown(Schema.Uint8ArrayFromSelf)(
189+
scriptRefRaw,
190+
)
191+
: undefined;
192+
193+
return new BabbageTransactionOutput({
194+
address,
195+
value,
196+
datumOption,
197+
scriptRef,
198+
});
199+
});
99200
}),
100201
),
101-
decode: (fromA, options, ast, fromI) =>
102-
Effect.gen(function* () {
103-
const decoded = yield* CBOR.decodeBytes(fromA).pipe(
104-
Effect.mapError(
105-
(error) => new ParseResult.Type(ast, fromA, error.message),
106-
),
107-
);
108-
109-
if (!(decoded instanceof Map)) {
110-
return yield* ParseResult.fail(
111-
new ParseResult.Type(ast, fromA, "Expected CBOR Map"),
112-
);
113-
}
114-
115-
const addressBytes = decoded.get(0) as Uint8Array;
116-
const value = decoded.get(1) as bigint;
117-
const datumOption = decoded.get(2) as Uint8Array | undefined;
118-
const scriptRef = decoded.get(3) as Uint8Array | undefined;
119-
120-
const address = yield* ParseResult.decodeUnknown(Address.Address)(
121-
addressBytes,
122-
);
123-
124-
return new BabbageTransactionOutput({
125-
address,
126-
value,
127-
datumOption,
128-
scriptRef,
129-
});
130-
}),
131202
},
132203
);
133204

134205
/**
135-
* Schema for transforming between CBOR hex and BabbageTransactionOutput
206+
* Schema for transforming between CBOR hex string and BabbageTransactionOutput.
207+
* This schema handles encoding BabbageTransactionOutput to a CBOR hex string
208+
* and decoding a CBOR hex string back into a BabbageTransactionOutput.
209+
*
210+
* @example
211+
* import { BabbageTransactionOutput } from "@lucid-evolution/experimental/BabbageTransactionOutput";
212+
* import * as Hex from "@lucid-evolution/experimental/Hex";
213+
* import { Effect } from "effect";
214+
* import assert from "assert";
215+
*
216+
* const exampleHex = "a400581c810477813a4362d26f6323a63339f4083a62885994b7c617b010000000000000001028200581ca55a305d233157b545d13783a649842f21950c41031c260386e81e370358204618e4740e53a31c51082c5f110753066d525287f3b8908f906f32e60000000000000003"; // A complex example
217+
* const cborHexResult = BabbageTransactionOutput.CBORHex.decode(Hex.makeOrThrow(exampleHex));
218+
* const decodedOutput = Effect.runSync(cborHexResult);
219+
*
220+
* assert(decodedOutput.address._tag === "BaseAddress");
221+
* assert(decodedOutput.value === 1n);
222+
* assert(decodedOutput.datumOption?._tag === "Hash");
223+
* assert(decodedOutput.scriptRef !== undefined);
136224
*
137225
* @since 2.0.0
138226
* @category encoding/decoding
@@ -154,6 +242,24 @@ export const CBORHex = Schema.transformOrFail(
154242
/**
155243
* Check if two BabbageTransactionOutput instances are equal.
156244
*
245+
* @example
246+
* import { BabbageTransactionOutput } from "@lucid-evolution/experimental/BabbageTransactionOutput";
247+
* import * as Address from "@lucid-evolution/experimental/Address";
248+
* import * as Hex from "@lucid-evolution/experimental/Hex";
249+
* import assert from "assert";
250+
*
251+
* const addr1 = Address.BaseAddress.makeOrThrow({
252+
* payment: { _tag: "KeyHash", hash: Hex.makeOrThrow("c37b1b5dc0669f1d3c61a6fddb2e8fde96be87b881c60bce8e8d542f") },
253+
* stake: { _tag: "ScriptHash", hash: Hex.makeOrThrow("1a4a40d588523c1186716a505b3191f6368d40026e6d1c810c90d6e2") },
254+
* network: "Preview"
255+
* });
256+
* const output1 = new BabbageTransactionOutput({ address: addr1, value: 100n });
257+
* const output2 = new BabbageTransactionOutput({ address: addr1, value: 100n });
258+
* const output3 = new BabbageTransactionOutput({ address: addr1, value: 200n });
259+
*
260+
* assert(BabbageTransactionOutput.equals(output1, output2) === true);
261+
* assert(BabbageTransactionOutput.equals(output1, output3) === false);
262+
*
157263
* @since 2.0.0
158264
* @category equality
159265
*/
@@ -171,15 +277,27 @@ export const equals = (
171277
};
172278

173279
/**
174-
* Generator for creating random BabbageTransactionOutput instances for testing
280+
* Generate a random BabbageTransactionOutput instance for property-based testing.
281+
*
282+
* @example
283+
* import { BabbageTransactionOutput } from "@lucid-evolution/experimental/BabbageTransactionOutput";
284+
* import { FastCheck } from "effect";
285+
* import assert from "assert";
286+
*
287+
* const randomSamples = FastCheck.sample(BabbageTransactionOutput.generator, 5);
288+
* randomSamples.forEach((output) => {
289+
* assert(output instanceof BabbageTransactionOutput);
290+
* assert(output.value >= 1n && output.value <= 1000000000n);
291+
* // Further assertions on address, datumOption, scriptRef can be added
292+
* });
175293
*
176294
* @since 2.0.0
177295
* @category generators
178296
*/
179297
export const generator = FastCheck.tuple(
180298
Address.generator,
181299
FastCheck.bigInt({ min: 1n, max: 1000000000n }),
182-
FastCheck.option(FastCheck.uint8Array({ minLength: 1, maxLength: 100 })),
300+
FastCheck.option(DatumOption.generator),
183301
FastCheck.option(FastCheck.uint8Array({ minLength: 1, maxLength: 200 })),
184302
).map(
185303
([address, value, datumOption, scriptRef]) =>

0 commit comments

Comments
 (0)