@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
219 lines (177 loc) • 6.64 kB
text/typescript
import {
alloc,
fromUtf8,
type Network,
networks,
type PublicKey,
toXOnly,
type XOnlyPublicKey,
} from '@btc-vision/bitcoin';
import { BinaryWriter } from '../buffer/BinaryWriter.js';
import {
type AccessListFeature,
type EpochSubmissionFeature,
type Feature,
Features,
type MLDSALinkRequest,
} from './Features.js';
import { Address } from '../keypair/Address.js';
import { Compressor } from '../bytecode/Compressor.js';
/** Bitcoin Script Generator */
export abstract class Generator {
/**
* The maximum size of a data chunk
*/
public static readonly DATA_CHUNK_SIZE: number = 512;
/**
* The magic number of OPNet
*/
public static readonly MAGIC: Uint8Array = fromUtf8('op');
/**
* The public key of the sender
* @protected
*/
protected readonly senderPubKey: Uint8Array;
/**
* The public key of the sender
* @protected
*/
protected readonly xSenderPubKey: Uint8Array;
/**
* The public key of the contract salt
* @protected
*/
protected readonly contractSaltPubKey?: Uint8Array | undefined;
/**
* The network to use
* @protected
*/
protected readonly network: Network = networks.bitcoin;
protected constructor(
senderPubKey: PublicKey | XOnlyPublicKey,
contractSaltPubKey?: Uint8Array,
network: Network = networks.bitcoin,
) {
this.senderPubKey = senderPubKey;
this.contractSaltPubKey = contractSaltPubKey;
this.network = network;
this.xSenderPubKey = toXOnly(senderPubKey);
}
public buildHeader(features: Features[]): Uint8Array {
let flags: number = 0;
for (const feature of features) {
flags |= feature;
}
const bytesU24 = alloc(3);
bytesU24[0] = (flags >> 16) & 0xff;
bytesU24[1] = (flags >> 8) & 0xff;
bytesU24[2] = flags & 0xff;
return Uint8Array.from([this.senderPubKey[0], ...bytesU24]);
}
public getHeader(maxPriority: bigint, features: Features[] = []): Uint8Array {
const writer = new BinaryWriter(12);
writer.writeBytes(this.buildHeader(features));
writer.writeU64(maxPriority);
return new Uint8Array(writer.getBuffer());
}
/**
* Compile the script
* @param args - The arguments to use when compiling the script
* @returns {Uint8Array} - The compiled script
*/
public abstract compile(...args: unknown[]): Uint8Array;
/**
* Split a buffer into chunks
* @param {Uint8Array} buffer - The buffer to split
* @param {number} chunkSize - The size of each chunk
* @protected
* @returns {Array<Uint8Array[]>} - The chunks
*/
protected splitBufferIntoChunks(
buffer: Uint8Array,
chunkSize: number = Generator.DATA_CHUNK_SIZE,
): Array<Uint8Array[]> {
const chunks: Array<Uint8Array[]> = [];
for (let i = 0; i < buffer.length; i += chunkSize) {
const dataLength = Math.min(chunkSize, buffer.length - i);
const buf2 = alloc(dataLength);
for (let j = 0; j < dataLength; j++) {
buf2[j] = buffer[i + j] as number;
}
chunks.push([buf2]);
}
return chunks;
}
protected encodeFeature(feature: Feature<Features>, finalBuffer: BinaryWriter): void {
switch (feature.opcode) {
case Features.ACCESS_LIST: {
return this.encodeAccessListFeature(feature as AccessListFeature, finalBuffer);
}
case Features.EPOCH_SUBMISSION: {
return this.encodeChallengeSubmission(
feature as EpochSubmissionFeature,
finalBuffer,
);
}
case Features.MLDSA_LINK_PUBKEY: {
return this.encodeLinkRequest(feature as MLDSALinkRequest, finalBuffer);
}
default:
throw new Error(`Unknown feature type: ${feature.opcode}`);
}
}
private encodeAccessListFeature(feature: AccessListFeature, finalBuffer: BinaryWriter): void {
const writer = new BinaryWriter();
writer.writeU16(Object.keys(feature.data).length);
for (const contract in feature.data) {
const parsedContract = Address.fromString(contract);
const data = feature.data[contract] as string[];
writer.writeAddress(parsedContract);
writer.writeU32(data.length);
for (const pointer of data) {
const pointerBuffer = Uint8Array.from(atob(pointer), (c) => c.charCodeAt(0));
if (pointerBuffer.length !== 32) {
throw new Error(`Invalid pointer length: ${pointerBuffer.length}`);
}
writer.writeBytes(pointerBuffer);
}
}
finalBuffer.writeBytesWithLength(Compressor.compress(new Uint8Array(writer.getBuffer())));
}
private encodeChallengeSubmission(
feature: EpochSubmissionFeature,
finalBuffer: BinaryWriter,
): void {
if ('verifySignature' in feature.data && !feature.data.verifySignature()) {
throw new Error('Invalid signature in challenge submission feature');
}
const writer = new BinaryWriter();
writer.writeBytes(feature.data.publicKey.toBuffer());
writer.writeBytes(feature.data.solution);
if (feature.data.graffiti) {
writer.writeBytesWithLength(feature.data.graffiti);
}
finalBuffer.writeBytesWithLength(writer.getBuffer());
}
private encodeLinkRequest(feature: MLDSALinkRequest, finalBuffer: BinaryWriter): void {
const data = feature.data;
const writer = new BinaryWriter();
writer.writeU8(data.level);
writer.writeBytes(data.hashedPublicKey);
writer.writeBoolean(data.verifyRequest);
if (data.verifyRequest) {
if (!data.publicKey || !data.mldsaSignature) {
throw new Error(
'MLDSA public key and signature required when verifyRequest is true',
);
}
writer.writeBytes(data.publicKey);
writer.writeBytes(data.mldsaSignature);
}
if (!data.legacySignature || data.legacySignature.length !== 64) {
throw new Error('Legacy signature must be exactly 64 bytes');
}
writer.writeBytes(data.legacySignature);
finalBuffer.writeBytesWithLength(writer.getBuffer());
}
}