UNPKG

@node-dlc/messaging

Version:
440 lines (372 loc) 14.2 kB
import { BufferReader, BufferWriter } from '@node-dlc/bufio'; import { ContractInfoType, MessageType } from '../MessageType'; import { ContractDescriptor, ContractDescriptorV0JSON, ContractDescriptorV1JSON, } from './ContractDescriptor'; import { DlcMessage, IDlcMessage } from './DlcMessage'; import { MultiOracleInfo, MultiOracleInfoJSON, OracleInfo, SingleOracleInfo, SingleOracleInfoJSON, } from './OracleInfo'; export abstract class ContractInfo extends DlcMessage { public static deserialize( buf: Buffer, ): SingleContractInfo | DisjointContractInfo { const reader = new BufferReader(buf); const typeId = Number(reader.readBigSize()); switch (typeId) { case ContractInfoType.Single: return SingleContractInfo.deserialize(buf); case ContractInfoType.Disjoint: return DisjointContractInfo.deserialize(buf); default: throw new Error( `Contract info type must be Single (0) or Disjoint (1), got ${typeId}`, ); } } /** * Creates a ContractInfo from JSON data (e.g., from test vectors) * @param json JSON object representing contract info */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any public static fromJSON(json: any): ContractInfo { if (!json) { throw new Error('contractInfo is required'); } // Check if it's a single contract info or disjoint contract info if (json.singleContractInfo || json.single_contract_info) { return SingleContractInfo.fromJSON( json.singleContractInfo || json.single_contract_info, ); } else if (json.disjointContractInfo || json.disjoint_contract_info) { return DisjointContractInfo.fromJSON( json.disjointContractInfo || json.disjoint_contract_info, ); } else { throw new Error( 'contractInfo must have either singleContractInfo or disjointContractInfo', ); } } public abstract contractInfoType: ContractInfoType; public abstract totalCollateral: bigint; public abstract validate(): void; public abstract toJSON(): ISingleContractInfoJSON | IDisjointContractInfoJSON; public abstract serialize(): Buffer; // Method to get total collateral (for compatibility) public getTotalCollateral(): bigint { return this.totalCollateral; } } /** * SingleContractInfo contains information about a contract's outcomes, * their corresponding payouts, and the oracles to be used. * This corresponds to the previous ContractInfoV0. */ export class SingleContractInfo extends ContractInfo implements IDlcMessage { public static contractInfoType = ContractInfoType.Single; public static type = MessageType.SingleContractInfo; /** * Creates a SingleContractInfo from JSON data * @param json JSON object representing single contract info */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any public static fromJSON(json: any): SingleContractInfo { const instance = new SingleContractInfo(); instance.totalCollateral = BigInt( json.totalCollateral || json.total_collateral || 0, ); // Handle nested contractInfo structure (test vectors have double nesting) const contractInfoData = json.contractInfo || json.contract_info || json; // Parse contract descriptor using proper fromJSON method instance.contractDescriptor = ContractDescriptor.fromJSON( contractInfoData.contractDescriptor || contractInfoData.contract_descriptor, ); // Parse oracle info using proper fromJSON method instance.oracleInfo = OracleInfo.fromJSON( contractInfoData.oracleInfo || contractInfoData.oracle_info, ); return instance; } /** * Deserializes a single_contract_info message * @param buf */ public static deserialize(buf: Buffer): SingleContractInfo { const instance = new SingleContractInfo(); const reader = new BufferReader(buf); reader.readBigSize(); // read type (0) instance.totalCollateral = reader.readUInt64BE(); // Read contract descriptor as sibling type (starts with its own type prefix) instance.contractDescriptor = ContractDescriptor.deserialize( Buffer.from(reader.buffer.subarray(reader.position)), ); // Skip past the contract descriptor we just read const descLength = instance.contractDescriptor.serialize().length; reader.position += descLength; // Read oracle info with rust-dlc format - discriminator + body const oracleType = Number(reader.readBigSize()); if (oracleType === 0) { // Single oracle instance.oracleInfo = SingleOracleInfo.deserializeBody( Buffer.from(reader.buffer.subarray(reader.position)), ); } else if (oracleType === 1) { // Multi oracle instance.oracleInfo = MultiOracleInfo.deserializeBody( Buffer.from(reader.buffer.subarray(reader.position)), ); } else { throw new Error(`Unknown oracle info type: ${oracleType}`); } return instance; } /** * The type for single_contract_info message - using MessageType for IDlcMessage compatibility */ public type = MessageType.SingleContractInfo; /** * The contract info type for new format */ public contractInfoType = ContractInfoType.Single; public totalCollateral: bigint; public contractDescriptor: ContractDescriptor; public oracleInfo: OracleInfo; // Compatibility property public get length(): bigint { return BigInt(this.serialize().length); } /** * Validates correctness of all fields in the message * @throws Will throw an error if validation fails */ public validate(): void { if (this.totalCollateral <= 0) { throw new Error('totalCollateral must be greater than 0'); } this.oracleInfo.validate(); // TODO: Add contract descriptor validation once available // this.contractDescriptor.validate(); } /** * Converts single_contract_info to JSON */ public toJSON(): ISingleContractInfoJSON { // Return enum variant format for Rust compatibility return { singleContractInfo: { totalCollateral: Number(this.totalCollateral), contractInfo: { contractDescriptor: this.contractDescriptor.toJSON(), oracleInfo: this.oracleInfo.toJSON(), }, }, }; } /** * Serializes the single_contract_info message into a Buffer */ public serialize(): Buffer { const writer = new BufferWriter(); writer.writeBigSize(this.contractInfoType); writer.writeUInt64BE(this.totalCollateral); writer.writeBytes(this.contractDescriptor.serialize()); // Use serializeBody() to match rust-dlc behavior - don't add extra TLV wrapper if (this.oracleInfo instanceof SingleOracleInfo) { writer.writeBigSize(0); // Single oracle discriminator writer.writeBytes(this.oracleInfo.serializeBody()); } else { writer.writeBigSize(1); // Multi oracle discriminator writer.writeBytes(this.oracleInfo.serializeBody()); } return writer.toBuffer(); } } /** * DisjointContractInfo contains information about multiple disjoint contract events. * This corresponds to the previous ContractInfoV1. */ export class DisjointContractInfo extends ContractInfo implements IDlcMessage { public static contractInfoType = ContractInfoType.Disjoint; public static type = MessageType.DisjointContractInfo; /** * Creates a DisjointContractInfo from JSON data * @param json JSON object representing disjoint contract info */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any public static fromJSON(json: any): DisjointContractInfo { const instance = new DisjointContractInfo(); instance.totalCollateral = BigInt( json.totalCollateral || json.total_collateral || 0, ); // Parse contract infos array const contractInfosData = json.contractInfos || json.contract_infos || []; instance.contractOraclePairs = contractInfosData.map( // eslint-disable-next-line @typescript-eslint/no-explicit-any (contractInfoData: any) => ({ contractDescriptor: ContractDescriptor.fromJSON( contractInfoData.contractDescriptor || contractInfoData.contract_descriptor, ), oracleInfo: OracleInfo.fromJSON( contractInfoData.oracleInfo || contractInfoData.oracle_info, ), }), ); return instance; } /** * Deserializes a disjoint_contract_info message * @param buf */ public static deserialize(buf: Buffer): DisjointContractInfo { const instance = new DisjointContractInfo(); const reader = new BufferReader(buf); reader.readBigSize(); // read type (1) instance.totalCollateral = reader.readUInt64BE(); const numDisjointEvents = Number(reader.readBigSize()); for (let i = 0; i < numDisjointEvents; i++) { // Read contract descriptor as sibling type (starts with its own type prefix) const contractDescriptor = ContractDescriptor.deserialize( Buffer.from(reader.buffer.subarray(reader.position)), ); // Skip past the contract descriptor we just read const descLength = contractDescriptor.serialize().length; reader.position += descLength; // Read oracle info with rust-dlc format - discriminator + body (same as SingleContractInfo) const oracleType = Number(reader.readBigSize()); let oracleInfo: OracleInfo; if (oracleType === 0) { // Single oracle oracleInfo = SingleOracleInfo.deserializeBody( Buffer.from(reader.buffer.subarray(reader.position)), ); } else if (oracleType === 1) { // Multi oracle oracleInfo = MultiOracleInfo.deserializeBody( Buffer.from(reader.buffer.subarray(reader.position)), ); } else { throw new Error(`Unknown oracle info type: ${oracleType}`); } // Skip past the oracle info we just read const oracleInfoLength = oracleInfo.serializeBody().length; reader.position += oracleInfoLength; instance.contractOraclePairs.push({ contractDescriptor, oracleInfo }); } return instance; } /** * The type for disjoint_contract_info message - using MessageType for IDlcMessage compatibility */ public type = MessageType.DisjointContractInfo; /** * The contract info type for new format */ public contractInfoType = ContractInfoType.Disjoint; public totalCollateral: bigint; public contractOraclePairs: IContractOraclePair[] = []; // Compatibility property public get length(): bigint { return BigInt(this.serialize().length); } /** * Validates correctness of all fields in the message * @throws Will throw an error if validation fails */ public validate(): void { if (this.totalCollateral <= 0) { throw new Error('totalCollateral must be greater than 0'); } if (this.contractOraclePairs.length === 0) { throw new Error('contractOraclePairs cannot be empty'); } this.contractOraclePairs.forEach((pair, index) => { try { pair.oracleInfo.validate(); // TODO: Add contract descriptor validation once available // pair.contractDescriptor.validate(); } catch (error) { throw new Error( `Validation failed for contract oracle pair ${index}: ${error.message}`, ); } }); } /** * Converts disjoint_contract_info to JSON */ public toJSON(): IDisjointContractInfoJSON { // Return enum variant format for Rust compatibility return { disjointContractInfo: { totalCollateral: Number(this.totalCollateral), contractInfos: this.contractOraclePairs.map((pair) => ({ contractDescriptor: pair.contractDescriptor.toJSON(), oracleInfo: pair.oracleInfo.toJSON(), })), }, }; } /** * Serializes the disjoint_contract_info message into a Buffer */ public serialize(): Buffer { const writer = new BufferWriter(); writer.writeBigSize(this.contractInfoType); writer.writeUInt64BE(this.totalCollateral); writer.writeBigSize(this.contractOraclePairs.length); for (const pair of this.contractOraclePairs) { writer.writeBytes(pair.contractDescriptor.serialize()); // Write oracle info with discriminator like SingleContractInfo does if (pair.oracleInfo instanceof SingleOracleInfo) { writer.writeBigSize(0); // Single oracle discriminator writer.writeBytes(pair.oracleInfo.serializeBody()); } else { writer.writeBigSize(1); // Multi oracle discriminator writer.writeBytes(pair.oracleInfo.serializeBody()); } } return writer.toBuffer(); } } // Legacy support - keeping old class names as aliases (both value and type exports) export const ContractInfoV0 = SingleContractInfo; export const ContractInfoV1 = DisjointContractInfo; export type ContractInfoV0 = SingleContractInfo; export type ContractInfoV1 = DisjointContractInfo; interface IContractOraclePair { contractDescriptor: ContractDescriptor; oracleInfo: OracleInfo; } interface IContractOraclePairJSON { contractDescriptor: ContractDescriptorV0JSON | ContractDescriptorV1JSON; oracleInfo: SingleOracleInfoJSON | MultiOracleInfoJSON; } // Rust-dlc enum variant format for SingleContractInfo export interface ISingleContractInfoJSON { singleContractInfo: { totalCollateral: number; contractInfo: { contractDescriptor: ContractDescriptorV0JSON | ContractDescriptorV1JSON; oracleInfo: SingleOracleInfoJSON | MultiOracleInfoJSON; }; }; } // Rust-dlc enum variant format for DisjointContractInfo export interface IDisjointContractInfoJSON { disjointContractInfo: { totalCollateral: number; contractInfos: IContractOraclePairJSON[]; }; } // Legacy type aliases for backward compatibility (same as the new interfaces) export type IContractInfoV0JSON = ISingleContractInfoJSON; export type IContractInfoV1JSON = IDisjointContractInfoJSON;