Skip to content
53 changes: 53 additions & 0 deletions src/sdk/utils/values/consensus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
consensusFieldsFrom,
observationValue,
observationError,
getAggregatedValue,
} from "./consensus";
import { val } from "./value";

Expand Down Expand Up @@ -86,6 +87,41 @@ describe("consensus helpers", () => {
expect(getAggregation(innerFields.Score!)).toBe(AggregationType.MEDIAN);
});

test("consensusFieldsFrom with mixed data types and aggregation strategies", () => {
const d = consensusFieldsFrom({
Foo: AggregationType.MEDIAN,
Bar: AggregationType.IDENTICAL,
Baz: AggregationType.COMMON_PREFIX,
});
expect(isFieldsMap(d)).toBe(true);
const map =
d.descriptor.case === "fieldsMap" ? d.descriptor.value.fields : {};
expect(Object.keys(map)).toEqual(["Foo", "Bar", "Baz"]);
expect(getAggregation(map.Foo!)).toBe(AggregationType.MEDIAN);
expect(getAggregation(map.Bar!)).toBe(AggregationType.IDENTICAL);
expect(getAggregation(map.Baz!)).toBe(AggregationType.COMMON_PREFIX);
});

test("consensusFieldsFrom with numeric enum values", () => {
const d = consensusFieldsFrom({
Foo: AggregationType.MEDIAN,
Bar: AggregationType.IDENTICAL,
});
expect(isFieldsMap(d)).toBe(true);
const map =
d.descriptor.case === "fieldsMap" ? d.descriptor.value.fields : {};
expect(getAggregation(map.Foo!)).toBe(AggregationType.MEDIAN);
expect(getAggregation(map.Bar!)).toBe(AggregationType.IDENTICAL);
});

test("consensusFieldsFrom with empty object", () => {
const d = consensusFieldsFrom({});
expect(isFieldsMap(d)).toBe(true);
const map =
d.descriptor.case === "fieldsMap" ? d.descriptor.value.fields : {};
expect(Object.keys(map)).toEqual([]);
});

test("observation helpers", () => {
const ov = observationValue(val.string("ok"));
expect(ov.case).toBe("value");
Expand All @@ -95,4 +131,21 @@ describe("consensus helpers", () => {
expect(oe.case).toBe("error");
expect(oe.value).toBe("boom");
});

test("getAggregatedValue with different consensus strategies", () => {
const priceInput = getAggregatedValue(val.float64(1850.5), "median");
expect(priceInput.observation.case).toBe("value");
expect(priceInput.descriptors).toBe(consensusDescriptorMedian);

const statusInput = getAggregatedValue(val.bool(true), "identical");
expect(statusInput.observation.case).toBe("value");
expect(statusInput.descriptors).toBe(consensusDescriptorIdentical);

const urlInput = getAggregatedValue(
val.string("https://api.example.com/v1/data"),
"commonPrefix"
);
expect(urlInput.observation.case).toBe("value");
expect(urlInput.descriptors).toBe(consensusDescriptorCommonPrefix);
});
});
72 changes: 72 additions & 0 deletions src/sdk/utils/values/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,65 @@ export const consensusFields = (fields: Record<string, ConsensusDescriptor>) =>
},
});

/**
* Creates a consensus fields descriptor from a mixed specification of aggregation types and descriptors.
*
* This function normalizes a mixed object where values can be either:
* - `AggregationType` enum values (numbers) - automatically converted to consensus descriptors
* - `ConsensusDescriptor` objects - used as-is
*
* @param spec - Object mapping field names to either AggregationType enum values or ConsensusDescriptor objects
* @returns A ConsensusDescriptor with a fieldsMap containing the normalized field descriptors
*
* @example
* ```typescript
* // Using AggregationType enum values (most common usage)
* const descriptors = consensusFieldsFrom({
* "Price": AggregationType.MEDIAN,
* "Volume": AggregationType.IDENTICAL
* });
*
* @example
* ```typescript
* // Mixed usage with pre-built descriptors
* const prebuilt = createConsensusDescriptorAggregation(AggregationType.COMMON_PREFIX);
* const descriptors = consensusFieldsFrom({
* "Foo": AggregationType.MEDIAN,
* "Bar": AggregationType.IDENTICAL,
* "Baz": prebuilt // Reuse pre-built descriptor
* });
*
* @example
* ```typescript
* // Nested field maps for complex consensus structures
* const nested = consensusFieldsFrom({
* "OuterField": consensusFieldsFrom({
* "InnerField": AggregationType.MEDIAN
* })
* });
*
* @example
* ```typescript
* // Real-world usage in workflow consensus inputs
* const consensusInput = create(SimpleConsensusInputsSchema, {
* observation: observationValue(val.mapValue({
* Foo: val.int64(response.fooValue),
* Bar: val.int64(response.barValue),
* Baz: val.string(response.bazValue)
* })),
* descriptors: consensusFieldsFrom({
* Foo: AggregationType.MEDIAN,
* Bar: AggregationType.IDENTICAL,
* Baz: AggregationType.COMMON_PREFIX,
* }),
* default: val.mapValue({
* Foo: val.int64(42),
* Bar: val.int64(123),
* Baz: val.string("default"),
* }),
* });
* ```
*/
export const consensusFieldsFrom = (
spec: Record<string, ConsensusDescriptor | AggregationType>
) => {
Expand Down Expand Up @@ -121,6 +180,10 @@ export const observationError = (message: string): ObservationErrorCase => ({
* among multiple oracle nodes. Each consensus strategy aggregates observations
* differently to produce a single trusted result.
*
* **Note**: This function only handles single values with a single consensus strategy.
* For complex objects with multiple fields using different consensus strategies,
* use `consensusFieldsFrom` directly with `create(SimpleConsensusInputsSchema, ...)`.
*
* @param value - The observation value from this oracle node
* @param consensus - The consensus mechanism to use for aggregation:
* - `"median"` - Takes middle value when sorted (ideal for numerical data, like prices)
Expand All @@ -139,6 +202,15 @@ export const observationError = (message: string): ObservationErrorCase => ({
*
* // API endpoint - find common base URL
* const urlInput = getAggregatedValue(val.string("https://api.example.com/v1/data"), "commonPrefix");
*
* // For complex multi-field consensus, use consensusFieldsFrom instead:
* // const complexInput = create(SimpleConsensusInputsSchema, {
* // observation: observationValue(val.mapValue({ Foo: val.int64(42), Bar: val.string("test") })),
* // descriptors: consensusFieldsFrom({
* // Foo: AggregationType.MEDIAN,
* // Bar: AggregationType.IDENTICAL,
* // }),
* // });
* ```
*/
export const getAggregatedValue = (
Expand Down
Loading