@node-dlc/messaging
Version:
DLC Messaging Protocol
527 lines (439 loc) • 16.8 kB
text/typescript
import { BufferReader, BufferWriter } from '@node-dlc/bufio';
import { MessageType } from '../MessageType';
import { getTlv } from '../serialize/getTlv';
import { IDlcMessage } from './DlcMessage';
import {
OracleAnnouncement,
OracleAnnouncementJSON,
} from './OracleAnnouncement';
/**
* OracleParams describe allowed differences between oracles in
* numerical outcome contracts, as per rust-dlc specification.
*/
export class OracleParams implements IDlcMessage {
public static type = MessageType.OracleParamsV0;
/**
* Creates an OracleParams from JSON data
* @param json JSON object representing oracle params
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
public static fromJSON(json: any): OracleParams {
const instance = new OracleParams();
instance.maxErrorExp = json.maxErrorExp || json.max_error_exp || 0;
instance.minFailExp = json.minFailExp || json.min_fail_exp || 0;
instance.maximizeCoverage =
json.maximizeCoverage || json.maximize_coverage || false;
return instance;
}
/**
* Deserializes oracle_params_v0 message
*/
public static deserialize(buf: Buffer): OracleParams {
const instance = new OracleParams();
const reader = new BufferReader(buf);
reader.readBigSize(); // read type
instance.length = reader.readBigSize();
instance.maxErrorExp = reader.readUInt16BE();
instance.minFailExp = reader.readUInt16BE();
instance.maximizeCoverage = reader.readUInt8() === 1;
return instance;
}
/**
* Deserializes oracle params body without TLV wrapper (for optional deserialization)
*/
public static deserializeBody(buf: Buffer): OracleParams {
const instance = new OracleParams();
const reader = new BufferReader(buf);
// No type/length to read - just the body content
instance.maxErrorExp = reader.readUInt16BE();
instance.minFailExp = reader.readUInt16BE();
instance.maximizeCoverage = reader.readUInt8() === 1;
return instance;
}
public type = OracleParams.type;
public length: bigint;
/** The maximum allowed difference between oracle expressed as a power of 2. */
public maxErrorExp: number;
/** The minimum allowed difference that should be supported by the contract expressed as a power of 2. */
public minFailExp: number;
/** Whether to maximize coverage of the interval between maxErrorExp and minFailExp. */
public maximizeCoverage: boolean;
public validate(): void {
if (this.maxErrorExp < 0) {
throw new Error('maxErrorExp must be greater than or equal to 0');
}
if (this.minFailExp < 0) {
throw new Error('minFailExp must be greater than or equal to 0');
}
if (this.maxErrorExp >= this.minFailExp) {
throw new Error('maxErrorExp must be less than minFailExp');
}
}
public toJSON(): OracleParamsJSON {
return {
maxErrorExp: this.maxErrorExp,
minFailExp: this.minFailExp,
maximizeCoverage: this.maximizeCoverage,
};
}
public serialize(): Buffer {
const writer = new BufferWriter();
writer.writeBigSize(this.type);
const dataWriter = new BufferWriter();
dataWriter.writeUInt16BE(this.maxErrorExp);
dataWriter.writeUInt16BE(this.minFailExp);
dataWriter.writeUInt8(this.maximizeCoverage ? 1 : 0);
writer.writeBigSize(dataWriter.size);
writer.writeBytes(dataWriter.toBuffer());
return writer.toBuffer();
}
/**
* Serializes the oracle params body without TLV wrapper (for optional serialization)
*/
public serializeBody(): Buffer {
const writer = new BufferWriter();
writer.writeUInt16BE(this.maxErrorExp);
writer.writeUInt16BE(this.minFailExp);
writer.writeUInt8(this.maximizeCoverage ? 1 : 0);
return writer.toBuffer();
}
}
/**
* SingleOracleInfo contains information about a single oracle.
*/
export class SingleOracleInfo implements IDlcMessage {
public static type = MessageType.SingleOracleInfo;
/**
* Creates a SingleOracleInfo from JSON data
* @param json JSON object representing single oracle info
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
public static fromJSON(json: any): SingleOracleInfo {
const instance = new SingleOracleInfo();
const announcementData = json.announcement || json.oracleAnnouncement;
if (!announcementData) {
throw new Error(
'announcement or oracleAnnouncement is required for single oracle info',
);
}
// Parse announcement using proper fromJSON method
instance.announcement = OracleAnnouncement.fromJSON(announcementData);
return instance;
}
/**
* Deserializes single oracle info
*/
public static deserialize(buf: Buffer): SingleOracleInfo {
const instance = new SingleOracleInfo();
const reader = new BufferReader(buf);
// Read the type and length that serialize() writes
reader.readBigSize(); // read type
instance.length = reader.readBigSize(); // read length
// Read the announcement data
instance.announcement = OracleAnnouncement.deserialize(
reader.readBytes(Number(instance.length)),
);
return instance;
}
public type = SingleOracleInfo.type;
public length: bigint;
/** The oracle announcement from the oracle. */
public announcement: OracleAnnouncement;
/**
* Returns the closest maturity date amongst all events
*/
public getClosestMaturityDate(): number {
return this.announcement.oracleEvent.eventMaturityEpoch;
}
public validate(): void {
this.announcement.validate();
}
public toJSON(): SingleOracleInfoJSON {
// Return enum variant format for Rust compatibility
return {
single: {
oracleAnnouncement: this.announcement.toJSON(),
},
};
}
public serialize(): Buffer {
const writer = new BufferWriter();
writer.writeBigSize(this.type);
const dataWriter = new BufferWriter();
dataWriter.writeBytes(this.announcement.serialize());
writer.writeBigSize(dataWriter.size);
writer.writeBytes(dataWriter.toBuffer());
return writer.toBuffer();
}
/**
* Serializes the body without TLV wrapper (for embedding in ContractInfo)
* This matches rust-dlc behavior where OracleInfo.write() doesn't add type_id
*/
public serializeBody(): Buffer {
return this.announcement.serialize();
}
/**
* Deserializes the body without TLV wrapper (for embedding in ContractInfo)
* This matches rust-dlc behavior where OracleInfo is read without type_id
*/
public static deserializeBody(buf: Buffer): SingleOracleInfo {
const instance = new SingleOracleInfo();
// No type/length to read - just the announcement data directly
instance.announcement = OracleAnnouncement.deserialize(buf);
return instance;
}
}
/**
* MultiOracleInfo contains information about multiple oracles used in multi-oracle based contracts.
*/
export class MultiOracleInfo implements IDlcMessage {
public static type = MessageType.MultiOracleInfo;
/**
* Creates a MultiOracleInfo from JSON data
* @param json JSON object representing multi oracle info
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
public static fromJSON(json: any): MultiOracleInfo {
const instance = new MultiOracleInfo();
instance.threshold = json.threshold || 1;
// Parse oracle announcements using proper fromJSON method
const announcements =
json.oracleAnnouncements || json.oracle_announcements || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
instance.announcements = announcements.map((announcementJson: any) =>
OracleAnnouncement.fromJSON(announcementJson),
);
// Parse oracle params if present (null means explicitly absent)
const oracleParamsData = json.oracleParams || json.oracle_params;
if (oracleParamsData !== null && oracleParamsData !== undefined) {
// Create OracleParams from JSON data using the fromJSON method
instance.oracleParams = OracleParams.fromJSON(oracleParamsData);
} else {
// Explicitly null/undefined - will serialize as 00 (not present)
instance.oracleParams = undefined;
}
return instance;
}
/**
* Deserializes multi oracle info
*/
public static deserialize(buf: Buffer): MultiOracleInfo {
const instance = new MultiOracleInfo();
const reader = new BufferReader(buf);
// Read the type and length that serialize() writes
reader.readBigSize(); // read type
instance.length = reader.readBigSize(); // read length
// In rust-dlc format, MultiOracleInfo body is: threshold + announcements + optional oracle params
instance.threshold = reader.readUInt16BE();
const numAnnouncements = Number(reader.readBigSize()); // Changed from readUInt16BE to readBigSize to match rust-dlc vec_cb
for (let i = 0; i < numAnnouncements; i++) {
instance.announcements.push(
OracleAnnouncement.deserialize(getTlv(reader)),
);
}
// Optional oracle params using Optional sub-type format
const oracleParamsData = reader.readOptional();
if (oracleParamsData) {
instance.oracleParams = OracleParams.deserializeBody(oracleParamsData);
}
return instance;
}
public type = MultiOracleInfo.type;
public length: bigint;
/** The threshold to be used for the contract (e.g. 2 of 3). */
public threshold: number;
/** The set of oracle announcements. */
public announcements: OracleAnnouncement[] = [];
/** The parameters to be used when allowing differences between oracle outcomes in numerical outcome contracts. */
public oracleParams?: OracleParams;
public validate(): void {
if (this.threshold <= 0) {
throw new Error('threshold must be greater than 0');
}
if (this.threshold > this.announcements.length) {
throw new Error(
'threshold cannot be greater than number of announcements',
);
}
if (this.announcements.length === 0) {
throw new Error('must have at least one announcement');
}
// Validate all announcements
this.announcements.forEach((announcement) => announcement.validate());
// Validate oracle params if present
if (this.oracleParams) {
this.oracleParams.validate();
}
}
/**
* Returns the closest maturity date amongst all events
*/
public getClosestMaturityDate(): number {
return Math.min(
...this.announcements.map((a) => a.oracleEvent.eventMaturityEpoch),
);
}
public toJSON(): MultiOracleInfoJSON {
// Return enum variant format for Rust compatibility
return {
multi: {
threshold: this.threshold,
oracleAnnouncements: this.announcements.map((a) => a.toJSON()),
oracleParams: this.oracleParams?.toJSON(),
},
};
}
public serialize(): Buffer {
const writer = new BufferWriter();
// writer.writeBigSize(this.type);
const dataWriter = new BufferWriter();
dataWriter.writeUInt16BE(this.threshold);
dataWriter.writeBigSize(this.announcements.length); // Changed from writeUInt16BE to writeBigSize to match rust-dlc vec_cb
for (const announcement of this.announcements) {
dataWriter.writeBytes(announcement.serialize());
}
// Use Optional serialization for oracle params (body content only, not TLV wrapped)
const oracleParamsData = this.oracleParams
? this.oracleParams.serializeBody()
: null;
dataWriter.writeOptional(oracleParamsData);
// writer.writeBigSize(dataWriter.size);
writer.writeBytes(dataWriter.toBuffer());
return writer.toBuffer();
}
/**
* Serializes the body without TLV wrapper (for embedding in ContractInfo)
* This matches rust-dlc behavior where OracleInfo.write() doesn't add type_id
*/
public serializeBody(): Buffer {
const writer = new BufferWriter();
writer.writeUInt16BE(this.threshold);
writer.writeBigSize(this.announcements.length); // Changed from writeUInt16BE to writeBigSize to match rust-dlc vec_cb
for (const announcement of this.announcements) {
writer.writeBytes(announcement.serialize());
}
// Use Optional serialization for oracle params (body content only, not TLV wrapped)
const oracleParamsData = this.oracleParams
? this.oracleParams.serializeBody()
: null;
writer.writeOptional(oracleParamsData);
return writer.toBuffer();
}
/**
* Deserializes the body without TLV wrapper (for embedding in ContractInfo)
* This matches rust-dlc behavior where OracleInfo is read without type_id
*/
public static deserializeBody(buf: Buffer): MultiOracleInfo {
const instance = new MultiOracleInfo();
const reader = new BufferReader(buf);
// No type/length to read - directly read the multi-oracle body
instance.threshold = reader.readUInt16BE();
const numAnnouncements = Number(reader.readBigSize()); // BigSize for announcements count
for (let i = 0; i < numAnnouncements; i++) {
instance.announcements.push(
OracleAnnouncement.deserialize(getTlv(reader)),
);
}
// Optional oracle params using Optional sub-type format
const oracleParamsData = reader.readOptional();
if (oracleParamsData) {
instance.oracleParams = OracleParams.deserializeBody(oracleParamsData);
}
return instance;
}
}
/**
* OracleInfo contains information about the oracles to be used in
* executing a DLC. Updated to support both single and multi-oracle
* patterns as per rust-dlc specification.
*/
export abstract class OracleInfo implements IDlcMessage {
public static deserialize(buf: Buffer): SingleOracleInfo | MultiOracleInfo {
const reader = new BufferReader(buf);
const type = Number(reader.readBigSize());
switch (type) {
case MessageType.SingleOracleInfo:
return SingleOracleInfo.deserialize(buf);
case MessageType.MultiOracleInfo:
return MultiOracleInfo.deserialize(buf);
default:
throw new Error(`Unknown oracle info type: ${type}`);
}
}
/**
* Creates an OracleInfo from JSON data (e.g., from test vectors)
* @param json JSON object representing oracle info
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
public static fromJSON(json: any): OracleInfo {
if (!json) {
throw new Error('oracleInfo is required');
}
// Handle direct single oracle (legacy format)
if (json.announcement) {
return SingleOracleInfo.fromJSON(json);
}
// Handle wrapped single oracle
else if (json.single) {
return SingleOracleInfo.fromJSON(json.single);
}
// Handle multi oracle
else if (json.multi) {
return MultiOracleInfo.fromJSON(json.multi);
} else {
throw new Error(
'oracleInfo must have either announcement, single, or multi',
);
}
}
public abstract type: number;
public abstract validate(): void;
public abstract toJSON(): SingleOracleInfoJSON | MultiOracleInfoJSON;
public abstract serialize(): Buffer;
public abstract serializeBody(): Buffer;
/**
* Returns the closest maturity date amongst all events
*/
public getClosestMaturityDate(): number {
if (this instanceof SingleOracleInfo) {
return this.announcement.oracleEvent.eventMaturityEpoch;
} else if (this instanceof MultiOracleInfo) {
return this.getClosestMaturityDate();
}
throw new Error('Unknown oracle info type');
}
}
// For backward compatibility, keep the V0 class as alias to SingleOracleInfo
export class OracleInfoV0 extends SingleOracleInfo {
/**
* Creates an OracleInfoV0 from JSON data (alias for SingleOracleInfo.fromJSON)
* @param json JSON object representing oracle info
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
public static fromJSON(json: any): OracleInfoV0 {
return SingleOracleInfo.fromJSON(json) as OracleInfoV0;
}
}
// JSON interfaces
export interface OracleParamsJSON {
type?: number; // Made optional for rust-dlc compatibility
maxErrorExp: number;
minFailExp: number;
maximizeCoverage: boolean;
}
// Rust-dlc enum variant format for SingleOracleInfo
export interface SingleOracleInfoJSON {
single: {
oracleAnnouncement: OracleAnnouncementJSON;
};
}
// Rust-dlc enum variant format for MultiOracleInfo
export interface MultiOracleInfoJSON {
multi: {
threshold: number;
oracleAnnouncements: OracleAnnouncementJSON[];
oracleParams?: OracleParamsJSON;
};
}
// Backward compatibility type alias
export type OracleInfoV0JSON = SingleOracleInfoJSON;